|
|
|
|
SSL 0091.patch
Interaktion und Portierbarkeitunbekannt
|
|
Spracherkennung für: .patch vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
From: Nico Grunbaum <na-g@nostrum.com>
Date: Fri, 30 Apr 2021 21:51:00 +0000
Subject: Bug 1654112 - Add grit dep for building webrtc on android; r=mjf
Differential Revision: https://phabricator.services.mozilla.com/D114027
Mercurial Revision: https://hg.mozilla.org/mozilla-central/rev/3cce5e6938f0df87bd9ab12a5f556aceb93dfa1d
---
tools/grit/.gitignore | 1 +
tools/grit/BUILD.gn | 48 +
tools/grit/MANIFEST.in | 3 +
tools/grit/OWNERS | 8 +
tools/grit/PRESUBMIT.py | 22 +
tools/grit/README.md | 19 +
tools/grit/grit.py | 31 +
tools/grit/grit/__init__.py | 19 +
tools/grit/grit/clique.py | 491 +++
tools/grit/grit/clique_unittest.py | 265 ++
tools/grit/grit/constants.py | 23 +
tools/grit/grit/exception.py | 139 +
tools/grit/grit/extern/BogoFP.py | 22 +
tools/grit/grit/extern/FP.py | 72 +
tools/grit/grit/extern/__init__.py | 0
tools/grit/grit/extern/tclib.py | 503 +++
tools/grit/grit/format/__init__.py | 8 +
tools/grit/grit/format/android_xml.py | 212 ++
.../grit/grit/format/android_xml_unittest.py | 149 +
tools/grit/grit/format/c_format.py | 95 +
tools/grit/grit/format/c_format_unittest.py | 81 +
.../grit/grit/format/chrome_messages_json.py | 59 +
.../format/chrome_messages_json_unittest.py | 190 +
tools/grit/grit/format/data_pack.py | 321 ++
tools/grit/grit/format/data_pack_unittest.py | 102 +
.../grit/grit/format/gen_predetermined_ids.py | 144 +
.../format/gen_predetermined_ids_unittest.py | 46 +
tools/grit/grit/format/gzip_string.py | 46 +
.../grit/grit/format/gzip_string_unittest.py | 65 +
tools/grit/grit/format/html_inline.py | 602 ++++
.../grit/grit/format/html_inline_unittest.py | 927 +++++
tools/grit/grit/format/minifier.py | 45 +
.../grit/grit/format/policy_templates_json.py | 26 +
.../format/policy_templates_json_unittest.py | 207 ++
tools/grit/grit/format/rc.py | 474 +++
tools/grit/grit/format/rc_header.py | 48 +
tools/grit/grit/format/rc_header_unittest.py | 138 +
tools/grit/grit/format/rc_unittest.py | 415 +++
tools/grit/grit/format/resource_map.py | 159 +
.../grit/grit/format/resource_map_unittest.py | 345 ++
tools/grit/grit/gather/__init__.py | 8 +
tools/grit/grit/gather/admin_template.py | 62 +
.../grit/gather/admin_template_unittest.py | 115 +
tools/grit/grit/gather/chrome_html.py | 377 ++
.../grit/grit/gather/chrome_html_unittest.py | 610 ++++
tools/grit/grit/gather/chrome_scaled_image.py | 157 +
.../gather/chrome_scaled_image_unittest.py | 209 ++
tools/grit/grit/gather/interface.py | 172 +
tools/grit/grit/gather/json_loader.py | 27 +
tools/grit/grit/gather/policy_json.py | 325 ++
.../grit/grit/gather/policy_json_unittest.py | 347 ++
tools/grit/grit/gather/rc.py | 343 ++
tools/grit/grit/gather/rc_unittest.py | 372 ++
tools/grit/grit/gather/regexp.py | 82 +
tools/grit/grit/gather/skeleton_gatherer.py | 149 +
tools/grit/grit/gather/tr_html.py | 743 ++++
tools/grit/grit/gather/tr_html_unittest.py | 524 +++
tools/grit/grit/gather/txt.py | 38 +
tools/grit/grit/gather/txt_unittest.py | 35 +
tools/grit/grit/grd_reader.py | 238 ++
tools/grit/grit/grd_reader_unittest.py | 346 ++
tools/grit/grit/grit-todo.xml | 62 +
tools/grit/grit/grit_runner.py | 334 ++
tools/grit/grit/grit_runner_unittest.py | 42 +
tools/grit/grit/lazy_re.py | 46 +
tools/grit/grit/lazy_re_unittest.py | 40 +
tools/grit/grit/node/__init__.py | 8 +
tools/grit/grit/node/base.py | 670 ++++
tools/grit/grit/node/base_unittest.py | 259 ++
tools/grit/grit/node/brotli_util.py | 29 +
tools/grit/grit/node/custom/__init__.py | 8 +
tools/grit/grit/node/custom/filename.py | 29 +
.../grit/node/custom/filename_unittest.py | 34 +
tools/grit/grit/node/empty.py | 64 +
tools/grit/grit/node/include.py | 170 +
tools/grit/grit/node/include_unittest.py | 134 +
tools/grit/grit/node/mapping.py | 60 +
tools/grit/grit/node/message.py | 362 ++
tools/grit/grit/node/message_unittest.py | 380 ++
tools/grit/grit/node/misc.py | 707 ++++
tools/grit/grit/node/misc_unittest.py | 590 ++++
tools/grit/grit/node/mock_brotli.py | 10 +
tools/grit/grit/node/node_io.py | 117 +
tools/grit/grit/node/node_io_unittest.py | 182 +
tools/grit/grit/node/structure.py | 375 ++
tools/grit/grit/node/structure_unittest.py | 178 +
tools/grit/grit/node/variant.py | 41 +
tools/grit/grit/pseudo.py | 129 +
tools/grit/grit/pseudo_rtl.py | 104 +
tools/grit/grit/pseudo_unittest.py | 55 +
tools/grit/grit/shortcuts.py | 93 +
tools/grit/grit/shortcuts_unittest.py | 79 +
tools/grit/grit/tclib.py | 246 ++
tools/grit/grit/tclib_unittest.py | 180 +
tools/grit/grit/test_suite_all.py | 34 +
tools/grit/grit/testdata/GoogleDesktop.adm | 945 +++++
tools/grit/grit/testdata/README.txt | 87 +
tools/grit/grit/testdata/about.html | 45 +
tools/grit/grit/testdata/android.xml | 24 +
tools/grit/grit/testdata/bad_browser.html | 16 +
tools/grit/grit/testdata/browser.html | 42 +
tools/grit/grit/testdata/buildinfo.grd | 46 +
tools/grit/grit/testdata/cache_prefix.html | 24 +
.../grit/grit/testdata/cache_prefix_file.html | 25 +
tools/grit/grit/testdata/chat_result.html | 24 +
.../chrome/app/generated_resources.grd | 199 ++
tools/grit/grit/testdata/chrome_html.html | 6 +
.../grit/testdata/default_100_percent/a.png | Bin 0 -> 159 bytes
.../grit/testdata/default_100_percent/b.png | 1 +
tools/grit/grit/testdata/del_footer.html | 8 +
tools/grit/grit/testdata/del_header.html | 60 +
tools/grit/grit/testdata/deleted.html | 21 +
tools/grit/grit/testdata/depfile.grd | 18 +
tools/grit/grit/testdata/details.html | 10 +
.../grit/testdata/duplicate-name-input.xml | 26 +
tools/grit/grit/testdata/email_result.html | 34 +
tools/grit/grit/testdata/email_thread.html | 10 +
tools/grit/grit/testdata/error.html | 8 +
tools/grit/grit/testdata/explicit_web.html | 11 +
tools/grit/grit/testdata/footer.html | 14 +
.../grit/testdata/generated_resources_fr.xtb | 3079 +++++++++++++++++
.../grit/testdata/generated_resources_iw.xtb | 4 +
.../grit/testdata/generated_resources_no.xtb | 4 +
tools/grit/grit/testdata/grit_part.grdp | 5 +
tools/grit/grit/testdata/header.html | 39 +
tools/grit/grit/testdata/homepage.html | 37 +
tools/grit/grit/testdata/hover.html | 177 +
tools/grit/grit/testdata/include_test.html | 31 +
tools/grit/grit/testdata/included_sample.html | 1 +
tools/grit/grit/testdata/indexing_speed.html | 58 +
tools/grit/grit/testdata/install_prefs.html | 92 +
tools/grit/grit/testdata/install_prefs2.html | 52 +
.../grit/testdata/klonk-alternate-skeleton.rc | Bin 0 -> 1088 bytes
tools/grit/grit/testdata/klonk.ico | Bin 0 -> 766 bytes
tools/grit/grit/testdata/klonk.rc | Bin 0 -> 9824 bytes
.../grit/grit/testdata/ko_oem_enable_bug.html | 1 +
.../grit/testdata/ko_oem_non_admin_bug.html | 1 +
tools/grit/grit/testdata/mini.html | 36 +
tools/grit/grit/testdata/oem_enable.html | 106 +
tools/grit/grit/testdata/oem_non_admin.html | 39 +
tools/grit/grit/testdata/onebox.html | 21 +
tools/grit/grit/testdata/oneclick.html | 34 +
tools/grit/grit/testdata/password.html | 37 +
tools/grit/grit/testdata/preferences.html | 234 ++
tools/grit/grit/testdata/preprocess_test.html | 7 +
tools/grit/grit/testdata/privacy.html | 35 +
tools/grit/grit/testdata/quit_apps.html | 49 +
tools/grit/grit/testdata/recrawl.html | 30 +
tools/grit/grit/testdata/resource_ids | 13 +
tools/grit/grit/testdata/script.html | 38 +
tools/grit/grit/testdata/searchbox.html | 22 +
tools/grit/grit/testdata/sidebar_h.html | 82 +
tools/grit/grit/testdata/sidebar_v.html | 267 ++
tools/grit/grit/testdata/simple-input.xml | 52 +
tools/grit/grit/testdata/simple.html | 3 +
tools/grit/grit/testdata/source.rc | 57 +
.../grit/testdata/special_100_percent/a.png | Bin 0 -> 159 bytes
tools/grit/grit/testdata/status.html | 44 +
.../grit/testdata/structure_variables.html | 4 +
tools/grit/grit/testdata/substitute.grd | 31 +
tools/grit/grit/testdata/substitute.xmb | 10 +
.../grit/grit/testdata/substitute_no_ids.grd | 31 +
tools/grit/grit/testdata/substitute_tmpl.grd | 31 +
tools/grit/grit/testdata/test_css.css | 1 +
tools/grit/grit/testdata/test_html.html | 1 +
tools/grit/grit/testdata/test_js.js | 1 +
tools/grit/grit/testdata/test_svg.svg | 1 +
tools/grit/grit/testdata/test_text.txt | 1 +
tools/grit/grit/testdata/time_related.html | 11 +
tools/grit/grit/testdata/toolbar_about.html | 138 +
.../grit/testdata/tools/grit/resource_ids | 176 +
tools/grit/grit/testdata/transl.rc | 56 +
tools/grit/grit/testdata/versions.html | 7 +
tools/grit/grit/testdata/whitelist.txt | 4 +
.../grit/testdata/whitelist_resources.grd | 54 +
.../grit/grit/testdata/whitelist_strings.grd | 23 +
tools/grit/grit/tool/__init__.py | 8 +
tools/grit/grit/tool/android2grd.py | 484 +++
tools/grit/grit/tool/android2grd_unittest.py | 181 +
tools/grit/grit/tool/build.py | 556 +++
tools/grit/grit/tool/build_unittest.py | 341 ++
tools/grit/grit/tool/buildinfo.py | 78 +
tools/grit/grit/tool/buildinfo_unittest.py | 90 +
tools/grit/grit/tool/count.py | 52 +
tools/grit/grit/tool/diff_structures.py | 119 +
.../grit/tool/diff_structures_unittest.py | 46 +
tools/grit/grit/tool/interface.py | 62 +
tools/grit/grit/tool/menu_from_parts.py | 79 +
tools/grit/grit/tool/newgrd.py | 85 +
tools/grit/grit/tool/newgrd_unittest.py | 51 +
tools/grit/grit/tool/postprocess_interface.py | 29 +
tools/grit/grit/tool/postprocess_unittest.py | 64 +
tools/grit/grit/tool/preprocess_interface.py | 25 +
tools/grit/grit/tool/preprocess_unittest.py | 50 +
tools/grit/grit/tool/rc2grd.py | 418 +++
tools/grit/grit/tool/rc2grd_unittest.py | 163 +
tools/grit/grit/tool/resize.py | 295 ++
tools/grit/grit/tool/test.py | 24 +
tools/grit/grit/tool/transl2tc.py | 251 ++
tools/grit/grit/tool/transl2tc_unittest.py | 133 +
tools/grit/grit/tool/unit.py | 43 +
.../grit/tool/update_resource_ids/__init__.py | 305 ++
.../grit/tool/update_resource_ids/assigner.py | 286 ++
.../update_resource_ids/assigner_unittest.py | 154 +
.../grit/tool/update_resource_ids/common.py | 101 +
.../grit/tool/update_resource_ids/parser.py | 231 ++
.../grit/tool/update_resource_ids/reader.py | 83 +
tools/grit/grit/tool/xmb.py | 295 ++
tools/grit/grit/tool/xmb_unittest.py | 132 +
tools/grit/grit/util.py | 691 ++++
tools/grit/grit/util_unittest.py | 118 +
tools/grit/grit/xtb_reader.py | 140 +
tools/grit/grit/xtb_reader_unittest.py | 110 +
tools/grit/grit_info.py | 173 +
tools/grit/grit_rule.gni | 485 +++
tools/grit/minify_with_uglify.py | 44 +
tools/grit/minimize_css.py | 105 +
tools/grit/minimize_css_unittest.py | 58 +
tools/grit/pak_util.py | 223 ++
tools/grit/repack.gni | 189 +
tools/grit/setup.py | 46 +
tools/grit/stamp_grit_sources.py | 57 +
tools/grit/third_party/six/LICENSE | 18 +
tools/grit/third_party/six/README | 16 +
tools/grit/third_party/six/README.chromium | 13 +
tools/grit/third_party/six/__init__.py | 868 +++++
226 files changed, 33440 insertions(+)
create mode 100644 tools/grit/.gitignore
create mode 100644 tools/grit/BUILD.gn
create mode 100644 tools/grit/MANIFEST.in
create mode 100644 tools/grit/OWNERS
create mode 100644 tools/grit/PRESUBMIT.py
create mode 100644 tools/grit/README.md
create mode 100644 tools/grit/grit.py
create mode 100644 tools/grit/grit/__init__.py
create mode 100644 tools/grit/grit/clique.py
create mode 100644 tools/grit/grit/clique_unittest.py
create mode 100644 tools/grit/grit/constants.py
create mode 100644 tools/grit/grit/exception.py
create mode 100644 tools/grit/grit/extern/BogoFP.py
create mode 100644 tools/grit/grit/extern/FP.py
create mode 100644 tools/grit/grit/extern/__init__.py
create mode 100644 tools/grit/grit/extern/tclib.py
create mode 100644 tools/grit/grit/format/__init__.py
create mode 100644 tools/grit/grit/format/android_xml.py
create mode 100644 tools/grit/grit/format/android_xml_unittest.py
create mode 100644 tools/grit/grit/format/c_format.py
create mode 100644 tools/grit/grit/format/c_format_unittest.py
create mode 100644 tools/grit/grit/format/chrome_messages_json.py
create mode 100644 tools/grit/grit/format/chrome_messages_json_unittest.py
create mode 100644 tools/grit/grit/format/data_pack.py
create mode 100644 tools/grit/grit/format/data_pack_unittest.py
create mode 100644 tools/grit/grit/format/gen_predetermined_ids.py
create mode 100644 tools/grit/grit/format/gen_predetermined_ids_unittest.py
create mode 100644 tools/grit/grit/format/gzip_string.py
create mode 100644 tools/grit/grit/format/gzip_string_unittest.py
create mode 100644 tools/grit/grit/format/html_inline.py
create mode 100644 tools/grit/grit/format/html_inline_unittest.py
create mode 100644 tools/grit/grit/format/minifier.py
create mode 100644 tools/grit/grit/format/policy_templates_json.py
create mode 100644 tools/grit/grit/format/policy_templates_json_unittest.py
create mode 100644 tools/grit/grit/format/rc.py
create mode 100644 tools/grit/grit/format/rc_header.py
create mode 100644 tools/grit/grit/format/rc_header_unittest.py
create mode 100644 tools/grit/grit/format/rc_unittest.py
create mode 100644 tools/grit/grit/format/resource_map.py
create mode 100644 tools/grit/grit/format/resource_map_unittest.py
create mode 100644 tools/grit/grit/gather/__init__.py
create mode 100644 tools/grit/grit/gather/admin_template.py
create mode 100644 tools/grit/grit/gather/admin_template_unittest.py
create mode 100644 tools/grit/grit/gather/chrome_html.py
create mode 100644 tools/grit/grit/gather/chrome_html_unittest.py
create mode 100644 tools/grit/grit/gather/chrome_scaled_image.py
create mode 100644 tools/grit/grit/gather/chrome_scaled_image_unittest.py
create mode 100644 tools/grit/grit/gather/interface.py
create mode 100644 tools/grit/grit/gather/json_loader.py
create mode 100644 tools/grit/grit/gather/policy_json.py
create mode 100644 tools/grit/grit/gather/policy_json_unittest.py
create mode 100644 tools/grit/grit/gather/rc.py
create mode 100644 tools/grit/grit/gather/rc_unittest.py
create mode 100644 tools/grit/grit/gather/regexp.py
create mode 100644 tools/grit/grit/gather/skeleton_gatherer.py
create mode 100644 tools/grit/grit/gather/tr_html.py
create mode 100644 tools/grit/grit/gather/tr_html_unittest.py
create mode 100644 tools/grit/grit/gather/txt.py
create mode 100644 tools/grit/grit/gather/txt_unittest.py
create mode 100644 tools/grit/grit/grd_reader.py
create mode 100644 tools/grit/grit/grd_reader_unittest.py
create mode 100644 tools/grit/grit/grit-todo.xml
create mode 100644 tools/grit/grit/grit_runner.py
create mode 100644 tools/grit/grit/grit_runner_unittest.py
create mode 100644 tools/grit/grit/lazy_re.py
create mode 100644 tools/grit/grit/lazy_re_unittest.py
create mode 100644 tools/grit/grit/node/__init__.py
create mode 100644 tools/grit/grit/node/base.py
create mode 100644 tools/grit/grit/node/base_unittest.py
create mode 100644 tools/grit/grit/node/brotli_util.py
create mode 100644 tools/grit/grit/node/custom/__init__.py
create mode 100644 tools/grit/grit/node/custom/filename.py
create mode 100644 tools/grit/grit/node/custom/filename_unittest.py
create mode 100644 tools/grit/grit/node/empty.py
create mode 100644 tools/grit/grit/node/include.py
create mode 100644 tools/grit/grit/node/include_unittest.py
create mode 100644 tools/grit/grit/node/mapping.py
create mode 100644 tools/grit/grit/node/message.py
create mode 100644 tools/grit/grit/node/message_unittest.py
create mode 100644 tools/grit/grit/node/misc.py
create mode 100644 tools/grit/grit/node/misc_unittest.py
create mode 100644 tools/grit/grit/node/mock_brotli.py
create mode 100644 tools/grit/grit/node/node_io.py
create mode 100644 tools/grit/grit/node/node_io_unittest.py
create mode 100644 tools/grit/grit/node/structure.py
create mode 100644 tools/grit/grit/node/structure_unittest.py
create mode 100644 tools/grit/grit/node/variant.py
create mode 100644 tools/grit/grit/pseudo.py
create mode 100644 tools/grit/grit/pseudo_rtl.py
create mode 100644 tools/grit/grit/pseudo_unittest.py
create mode 100644 tools/grit/grit/shortcuts.py
create mode 100644 tools/grit/grit/shortcuts_unittest.py
create mode 100644 tools/grit/grit/tclib.py
create mode 100644 tools/grit/grit/tclib_unittest.py
create mode 100644 tools/grit/grit/test_suite_all.py
create mode 100644 tools/grit/grit/testdata/GoogleDesktop.adm
create mode 100644 tools/grit/grit/testdata/README.txt
create mode 100644 tools/grit/grit/testdata/about.html
create mode 100644 tools/grit/grit/testdata/android.xml
create mode 100644 tools/grit/grit/testdata/bad_browser.html
create mode 100644 tools/grit/grit/testdata/browser.html
create mode 100644 tools/grit/grit/testdata/buildinfo.grd
create mode 100644 tools/grit/grit/testdata/cache_prefix.html
create mode 100644 tools/grit/grit/testdata/cache_prefix_file.html
create mode 100644 tools/grit/grit/testdata/chat_result.html
create mode 100644 tools/grit/grit/testdata/chrome/app/generated_resources.grd
create mode 100644 tools/grit/grit/testdata/chrome_html.html
create mode 100644 tools/grit/grit/testdata/default_100_percent/a.png
create mode 100644 tools/grit/grit/testdata/default_100_percent/b.png
create mode 100644 tools/grit/grit/testdata/del_footer.html
create mode 100644 tools/grit/grit/testdata/del_header.html
create mode 100644 tools/grit/grit/testdata/deleted.html
create mode 100644 tools/grit/grit/testdata/depfile.grd
create mode 100644 tools/grit/grit/testdata/details.html
create mode 100644 tools/grit/grit/testdata/duplicate-name-input.xml
create mode 100644 tools/grit/grit/testdata/email_result.html
create mode 100644 tools/grit/grit/testdata/email_thread.html
create mode 100644 tools/grit/grit/testdata/error.html
create mode 100644 tools/grit/grit/testdata/explicit_web.html
create mode 100644 tools/grit/grit/testdata/footer.html
create mode 100644 tools/grit/grit/testdata/generated_resources_fr.xtb
create mode 100644 tools/grit/grit/testdata/generated_resources_iw.xtb
create mode 100644 tools/grit/grit/testdata/generated_resources_no.xtb
create mode 100644 tools/grit/grit/testdata/grit_part.grdp
create mode 100644 tools/grit/grit/testdata/header.html
create mode 100644 tools/grit/grit/testdata/homepage.html
create mode 100644 tools/grit/grit/testdata/hover.html
create mode 100644 tools/grit/grit/testdata/include_test.html
create mode 100644 tools/grit/grit/testdata/included_sample.html
create mode 100644 tools/grit/grit/testdata/indexing_speed.html
create mode 100644 tools/grit/grit/testdata/install_prefs.html
create mode 100644 tools/grit/grit/testdata/install_prefs2.html
create mode 100644 tools/grit/grit/testdata/klonk-alternate-skeleton.rc
create mode 100644 tools/grit/grit/testdata/klonk.ico
create mode 100644 tools/grit/grit/testdata/klonk.rc
create mode 100644 tools/grit/grit/testdata/ko_oem_enable_bug.html
create mode 100644 tools/grit/grit/testdata/ko_oem_non_admin_bug.html
create mode 100644 tools/grit/grit/testdata/mini.html
create mode 100644 tools/grit/grit/testdata/oem_enable.html
create mode 100644 tools/grit/grit/testdata/oem_non_admin.html
create mode 100644 tools/grit/grit/testdata/onebox.html
create mode 100644 tools/grit/grit/testdata/oneclick.html
create mode 100644 tools/grit/grit/testdata/password.html
create mode 100644 tools/grit/grit/testdata/preferences.html
create mode 100644 tools/grit/grit/testdata/preprocess_test.html
create mode 100644 tools/grit/grit/testdata/privacy.html
create mode 100644 tools/grit/grit/testdata/quit_apps.html
create mode 100644 tools/grit/grit/testdata/recrawl.html
create mode 100644 tools/grit/grit/testdata/resource_ids
create mode 100644 tools/grit/grit/testdata/script.html
create mode 100644 tools/grit/grit/testdata/searchbox.html
create mode 100644 tools/grit/grit/testdata/sidebar_h.html
create mode 100644 tools/grit/grit/testdata/sidebar_v.html
create mode 100644 tools/grit/grit/testdata/simple-input.xml
create mode 100644 tools/grit/grit/testdata/simple.html
create mode 100644 tools/grit/grit/testdata/source.rc
create mode 100644 tools/grit/grit/testdata/special_100_percent/a.png
create mode 100644 tools/grit/grit/testdata/status.html
create mode 100644 tools/grit/grit/testdata/structure_variables.html
create mode 100644 tools/grit/grit/testdata/substitute.grd
create mode 100644 tools/grit/grit/testdata/substitute.xmb
create mode 100644 tools/grit/grit/testdata/substitute_no_ids.grd
create mode 100644 tools/grit/grit/testdata/substitute_tmpl.grd
create mode 100644 tools/grit/grit/testdata/test_css.css
create mode 100644 tools/grit/grit/testdata/test_html.html
create mode 100644 tools/grit/grit/testdata/test_js.js
create mode 100644 tools/grit/grit/testdata/test_svg.svg
create mode 100644 tools/grit/grit/testdata/test_text.txt
create mode 100644 tools/grit/grit/testdata/time_related.html
create mode 100644 tools/grit/grit/testdata/toolbar_about.html
create mode 100644 tools/grit/grit/testdata/tools/grit/resource_ids
create mode 100644 tools/grit/grit/testdata/transl.rc
create mode 100644 tools/grit/grit/testdata/versions.html
create mode 100644 tools/grit/grit/testdata/whitelist.txt
create mode 100644 tools/grit/grit/testdata/whitelist_resources.grd
create mode 100644 tools/grit/grit/testdata/whitelist_strings.grd
create mode 100644 tools/grit/grit/tool/__init__.py
create mode 100644 tools/grit/grit/tool/android2grd.py
create mode 100644 tools/grit/grit/tool/android2grd_unittest.py
create mode 100644 tools/grit/grit/tool/build.py
create mode 100644 tools/grit/grit/tool/build_unittest.py
create mode 100644 tools/grit/grit/tool/buildinfo.py
create mode 100644 tools/grit/grit/tool/buildinfo_unittest.py
create mode 100644 tools/grit/grit/tool/count.py
create mode 100644 tools/grit/grit/tool/diff_structures.py
create mode 100644 tools/grit/grit/tool/diff_structures_unittest.py
create mode 100644 tools/grit/grit/tool/interface.py
create mode 100644 tools/grit/grit/tool/menu_from_parts.py
create mode 100644 tools/grit/grit/tool/newgrd.py
create mode 100644 tools/grit/grit/tool/newgrd_unittest.py
create mode 100644 tools/grit/grit/tool/postprocess_interface.py
create mode 100644 tools/grit/grit/tool/postprocess_unittest.py
create mode 100644 tools/grit/grit/tool/preprocess_interface.py
create mode 100644 tools/grit/grit/tool/preprocess_unittest.py
create mode 100644 tools/grit/grit/tool/rc2grd.py
create mode 100644 tools/grit/grit/tool/rc2grd_unittest.py
create mode 100644 tools/grit/grit/tool/resize.py
create mode 100644 tools/grit/grit/tool/test.py
create mode 100644 tools/grit/grit/tool/transl2tc.py
create mode 100644 tools/grit/grit/tool/transl2tc_unittest.py
create mode 100644 tools/grit/grit/tool/unit.py
create mode 100644 tools/grit/grit/tool/update_resource_ids/__init__.py
create mode 100644 tools/grit/grit/tool/update_resource_ids/assigner.py
create mode 100644 tools/grit/grit/tool/update_resource_ids/assigner_unittest.py
create mode 100644 tools/grit/grit/tool/update_resource_ids/common.py
create mode 100644 tools/grit/grit/tool/update_resource_ids/parser.py
create mode 100644 tools/grit/grit/tool/update_resource_ids/reader.py
create mode 100644 tools/grit/grit/tool/xmb.py
create mode 100644 tools/grit/grit/tool/xmb_unittest.py
create mode 100644 tools/grit/grit/util.py
create mode 100644 tools/grit/grit/util_unittest.py
create mode 100644 tools/grit/grit/xtb_reader.py
create mode 100644 tools/grit/grit/xtb_reader_unittest.py
create mode 100644 tools/grit/grit_info.py
create mode 100644 tools/grit/grit_rule.gni
create mode 100644 tools/grit/minify_with_uglify.py
create mode 100644 tools/grit/minimize_css.py
create mode 100644 tools/grit/minimize_css_unittest.py
create mode 100644 tools/grit/pak_util.py
create mode 100644 tools/grit/repack.gni
create mode 100644 tools/grit/setup.py
create mode 100644 tools/grit/stamp_grit_sources.py
create mode 100644 tools/grit/third_party/six/LICENSE
create mode 100644 tools/grit/third_party/six/README
create mode 100644 tools/grit/third_party/six/README.chromium
create mode 100644 tools/grit/third_party/six/__init__.py
diff --git a/tools/grit/.gitignore b/tools/grit/.gitignore
new file mode 100644
index 0000000000..0d20b6487c
--- /dev/null
+++ b/tools/grit/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/tools/grit/BUILD.gn b/tools/grit/BUILD.gn
new file mode 100644
index 0000000000..1cd3c75b55
--- /dev/null
+++ b/tools/grit/BUILD.gn
@@ -0,0 +1,48 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This target creates a stamp file that depends on all the sources in the grit
+# directory. By depending on this, a target can force itself to be rebuilt if
+# grit itself changes.
+
+import("//build/config/sanitizers/sanitizers.gni")
+
+action("grit_sources") {
+ depfile = "$target_out_dir/grit_sources.d"
+ script = "stamp_grit_sources.py"
+
+ inputs = [ "grit.py" ]
+
+ # Note that we can't call this "grit_sources.stamp" because that file is
+ # implicitly created by GN for script actions.
+ outputs = [ "$target_out_dir/grit_sources.script.stamp" ]
+
+ args = [
+ rebase_path("//tools/grit", root_build_dir),
+ rebase_path(outputs[0], root_build_dir),
+ rebase_path(depfile, root_build_dir),
+ ]
+}
+
+group("grit_python_unittests") {
+ testonly = true
+
+ data = [
+ "//testing/scripts/common.py",
+ "//testing/scripts/run_isolated_script_test.py",
+ "//testing/xvfb.py",
+ "//tools/grit/",
+ "//third_party/catapult/third_party/typ/",
+ ]
+}
+
+# See https://crbug.com/983200
+if (is_mac && is_asan) {
+ create_bundle("brotli_mac_asan_workaround") {
+ bundle_root_dir = "$target_out_dir/$target_name"
+ bundle_executable_dir = bundle_root_dir
+
+ public_deps = [ "//third_party/brotli:brotli($host_toolchain)" ]
+ }
+}
diff --git a/tools/grit/MANIFEST.in b/tools/grit/MANIFEST.in
new file mode 100644
index 0000000000..1cbff42400
--- /dev/null
+++ b/tools/grit/MANIFEST.in
@@ -0,0 +1,3 @@
+exclude grit/test_suite_all.py
+exclude grit/tool/test.py
+global-exclude *_unittest.py
diff --git a/tools/grit/OWNERS b/tools/grit/OWNERS
new file mode 100644
index 0000000000..6a8f447b82
--- /dev/null
+++ b/tools/grit/OWNERS
@@ -0,0 +1,8 @@
+agrieve@chromium.org
+flackr@chromium.org
+thakis@chromium.org
+thestig@chromium.org
+
+# Admin policy related grit tools.
+per-file *policy*=file://components/policy/tools/OWNERS
+per-file *admin_template*=file://components/policy/tools/OWNERS
diff --git a/tools/grit/PRESUBMIT.py b/tools/grit/PRESUBMIT.py
new file mode 100644
index 0000000000..03b7188551
--- /dev/null
+++ b/tools/grit/PRESUBMIT.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""grit unittests presubmit script.
+
+See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts for
+details on the presubmit API built into gcl.
+"""
+
+
+def RunUnittests(input_api, output_api):
+ return input_api.canned_checks.RunUnitTests(input_api, output_api,
+ [input_api.os_path.join('grit', 'test_suite_all.py')])
+
+
+def CheckChangeOnUpload(input_api, output_api):
+ return RunUnittests(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+ return RunUnittests(input_api, output_api)
diff --git a/tools/grit/README.md b/tools/grit/README.md
new file mode 100644
index 0000000000..b5c3f4b51b
--- /dev/null
+++ b/tools/grit/README.md
@@ -0,0 +1,19 @@
+# GRIT (Google Resource and Internationalization Tool)
+
+This is a tool for projects to manage resources and simplify the localization
+workflow.
+
+See the user guide for more details on using this project:
+ https://dev.chromium.org/developers/tools-we-use-in-chromium/grit/grit-users-guide
+
+## History
+
+This code previously used to live at
+ https://code.google.com/p/grit-i18n/source/checkout which still contains the
+project's history. https://chromium.googlesource.com/external/grit-i18n/ is
+a git mirror of the SVN repository that's identical except for the last two
+commits. The project is now developed in the Chromium project directly.
+
+There is a read-only mirror of just this directory at
+ https://chromium.googlesource.com/chromium/src/tools/grit/ if you don't want to
+check out all of Chromium.
diff --git a/tools/grit/grit.py b/tools/grit/grit.py
new file mode 100644
index 0000000000..abd1ab6449
--- /dev/null
+++ b/tools/grit/grit.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Bootstrapping for GRIT.
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+
+import grit.grit_runner
+
+sys.path.append(
+ os.path.join(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+ 'diagnosis'))
+try:
+ import crbug_1001171
+except ImportError:
+ crbug_1001171 = None
+
+
+if __name__ == '__main__':
+ if crbug_1001171:
+ with crbug_1001171.DumpStateOnLookupError():
+ sys.exit(grit.grit_runner.Main(sys.argv[1:]))
+ else:
+ sys.exit(grit.grit_runner.Main(sys.argv[1:]))
diff --git a/tools/grit/grit/__init__.py b/tools/grit/grit/__init__.py
new file mode 100644
index 0000000000..91ac9ee896
--- /dev/null
+++ b/tools/grit/grit/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Package 'grit'
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+
+
+_CUR_DIR = os.path.abspath(os.path.dirname(__file__))
+_GRIT_DIR = os.path.dirname(_CUR_DIR)
+_THIRD_PARTY_DIR = os.path.join(_GRIT_DIR, 'third_party')
+
+if _THIRD_PARTY_DIR not in sys.path:
+ sys.path.insert(0, _THIRD_PARTY_DIR)
diff --git a/tools/grit/grit/clique.py b/tools/grit/grit/clique.py
new file mode 100644
index 0000000000..e7be3ec164
--- /dev/null
+++ b/tools/grit/grit/clique.py
@@ -0,0 +1,491 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Collections of messages and their translations, called cliques. Also
+collections of cliques (uber-cliques).
+'''
+
+from __future__ import print_function
+
+import re
+
+import six
+
+from grit import constants
+from grit import exception
+from grit import lazy_re
+from grit import pseudo
+from grit import pseudo_rtl
+from grit import tclib
+
+
+class UberClique(object):
+ '''A factory (NOT a singleton factory) for making cliques. It has several
+ methods for working with the cliques created using the factory.
+ '''
+
+ def __init__(self):
+ # A map from message ID to list of cliques whose source messages have
+ # that ID. This will contain all cliques created using this factory.
+ # Different messages can have the same ID because they have the
+ # same translateable portion and placeholder names, but occur in different
+ # places in the resource tree.
+ #
+ # Each list of cliques is kept sorted by description, to achieve
+ # stable results from the BestClique method, see below.
+ self.cliques_ = {}
+
+ # A map of clique IDs to list of languages to indicate translations where we
+ # fell back to English.
+ self.fallback_translations_ = {}
+
+ # A map of clique IDs to list of languages to indicate missing translations.
+ self.missing_translations_ = {}
+
+ def _AddMissingTranslation(self, lang, clique, is_error):
+ tl = self.fallback_translations_
+ if is_error:
+ tl = self.missing_translations_
+ id = clique.GetId()
+ if id not in tl:
+ tl[id] = {}
+ if lang not in tl[id]:
+ tl[id][lang] = 1
+
+ def HasMissingTranslations(self):
+ return len(self.missing_translations_) > 0
+
+ def MissingTranslationsReport(self):
+ '''Returns a string suitable for printing to report missing
+ and fallback translations to the user.
+ '''
+ def ReportTranslation(clique, langs):
+ text = clique.GetMessage().GetPresentableContent()
+ # The text 'error' (usually 'Error:' but we are conservative)
+ # can trigger some build environments (Visual Studio, we're
+ # looking at you) to consider invocation of grit to have failed,
+ # so we make sure never to output that word.
+ extract = re.sub(r'(?i)error', 'REDACTED', text[0:40])[0:40]
+ ellipsis = ''
+ if len(text) > 40:
+ ellipsis = '...'
+ langs_extract = langs[0:6]
+ describe_langs = ','.join(langs_extract)
+ if len(langs) > 6:
+ describe_langs += " and %d more" % (len(langs) - 6)
+ return " %s \"%s%s\" %s" % (clique.GetId(), extract, ellipsis,
+ describe_langs)
+ lines = []
+ if len(self.fallback_translations_):
+ lines.append(
+ "WARNING: Fell back to English for the following translations:")
+ for (id, langs) in self.fallback_translations_.items():
+ lines.append(
+ ReportTranslation(self.cliques_[id][0], list(langs.keys())))
+ if len(self.missing_translations_):
+ lines.append("ERROR: The following translations are MISSING:")
+ for (id, langs) in self.missing_translations_.items():
+ lines.append(
+ ReportTranslation(self.cliques_[id][0], list(langs.keys())))
+ return '\n'.join(lines)
+
+ def MakeClique(self, message, translateable=True):
+ '''Create a new clique initialized with a message.
+
+ Args:
+ message: tclib.Message()
+ translateable: True | False
+ '''
+ clique = MessageClique(self, message, translateable)
+
+ # Enable others to find this clique by its message ID
+ if message.GetId() in self.cliques_:
+ presentable_text = clique.GetMessage().GetPresentableContent()
+ if not message.HasAssignedId():
+ for c in self.cliques_[message.GetId()]:
+ assert c.GetMessage().GetPresentableContent() == presentable_text
+ self.cliques_[message.GetId()].append(clique)
+ # We need to keep each list of cliques sorted by description, to
+ # achieve stable results from the BestClique method, see below.
+ self.cliques_[message.GetId()].sort(
+ key=lambda c:c.GetMessage().GetDescription())
+ else:
+ self.cliques_[message.GetId()] = [clique]
+
+ return clique
+
+ def FindCliqueAndAddTranslation(self, translation, language):
+ '''Adds the specified translation to the clique with the source message
+ it is a translation of.
+
+ Args:
+ translation: tclib.Translation()
+ language: 'en' | 'fr' ...
+
+ Return:
+ True if the source message was found, otherwise false.
+ '''
+ if translation.GetId() in self.cliques_:
+ for clique in self.cliques_[translation.GetId()]:
+ clique.AddTranslation(translation, language)
+ return True
+ else:
+ return False
+
+ def BestClique(self, id):
+ '''Returns the "best" clique from a list of cliques. All the cliques
+ must have the same ID. The "best" clique is chosen in the following
+ order of preference:
+ - The first clique that has a non-ID-based description.
+ - If no such clique found, the first clique with an ID-based description.
+ - Otherwise the first clique.
+
+ This method is stable in terms of always returning a clique with
+ an identical description (on different runs of GRIT on the same
+ data) because self.cliques_ is sorted by description.
+ '''
+ clique_list = self.cliques_[id]
+ clique_with_id = None
+ clique_default = None
+ for clique in clique_list:
+ if not clique_default:
+ clique_default = clique
+
+ description = clique.GetMessage().GetDescription()
+ if description and len(description) > 0:
+ if not description.startswith('ID:'):
+ # this is the preferred case so we exit right away
+ return clique
+ elif not clique_with_id:
+ clique_with_id = clique
+ if clique_with_id:
+ return clique_with_id
+ else:
+ return clique_default
+
+ def BestCliquePerId(self):
+ '''Iterates over the list of all cliques and returns the best clique for
+ each ID. This will be the first clique with a source message that has a
+ non-empty description, or an arbitrary clique if none of them has a
+ description.
+ '''
+ for id in self.cliques_:
+ yield self.BestClique(id)
+
+ def BestCliqueByOriginalText(self, text, meaning):
+ '''Finds the "best" (as in BestClique()) clique that has original text
+ 'text' and meaning 'meaning'. Returns None if there is no such clique.
+ '''
+ # If needed, this can be optimized by maintaining a map of
+ # fingerprints of original text+meaning to cliques.
+ for c in self.BestCliquePerId():
+ msg = c.GetMessage()
+ if msg.GetRealContent() == text and msg.GetMeaning() == meaning:
+ return msg
+ return None
+
+ def AllMessageIds(self):
+ '''Returns a list of all defined message IDs.
+ '''
+ return list(self.cliques_.keys())
+
+ def AllCliques(self):
+ '''Iterates over all cliques. Note that this can return multiple cliques
+ with the same ID.
+ '''
+ for cliques in self.cliques_.values():
+ for c in cliques:
+ yield c
+
+ def GenerateXtbParserCallback(self, lang, debug=False):
+ '''Creates a callback function as required by grit.xtb_reader.Parse().
+ This callback will create Translation objects for each message from
+ the XTB that exists in this uberclique, and add them as translations for
+ the relevant cliques. The callback will add translations to the language
+ specified by 'lang'
+
+ Args:
+ lang: 'fr'
+ debug: True | False
+ '''
+ def Callback(id, structure):
+ if id not in self.cliques_:
+ if debug:
+ print("Ignoring translation #%s" % id)
+ return
+
+ if debug:
+ print("Adding translation #%s" % id)
+
+ # We fetch placeholder information from the original message (the XTB file
+ # only contains placeholder names).
+ original_msg = self.BestClique(id).GetMessage()
+
+ translation = tclib.Translation(id=id)
+ for is_ph,text in structure:
+ if not is_ph:
+ translation.AppendText(text)
+ else:
+ found_placeholder = False
+ for ph in original_msg.GetPlaceholders():
+ if ph.GetPresentation() == text:
+ translation.AppendPlaceholder(tclib.Placeholder(
+ ph.GetPresentation(), ph.GetOriginal(), ph.GetExample()))
+ found_placeholder = True
+ break
+ if not found_placeholder:
+ raise exception.MismatchingPlaceholders(
+ 'Translation for message ID %s had <ph name="%s"/>, no match\n'
+ 'in original message' % (id, text))
+ self.FindCliqueAndAddTranslation(translation, lang)
+ return Callback
+
+
+class CustomType(object):
+ '''A base class you should implement if you wish to specify a custom type
+ for a message clique (i.e. custom validation and optional modification of
+ translations).'''
+
+ def Validate(self, message):
+ '''Returns true if the message (a tclib.Message object) is valid,
+ otherwise false.
+ '''
+ raise NotImplementedError()
+
+ def ValidateAndModify(self, lang, translation):
+ '''Returns true if the translation (a tclib.Translation object) is valid,
+ otherwise false. The language is also passed in. This method may modify
+ the translation that is passed in, if it so wishes.
+ '''
+ raise NotImplementedError()
+
+ def ModifyTextPart(self, lang, text):
+ '''If you call ModifyEachTextPart, it will turn around and call this method
+ for each text part of the translation. You should return the modified
+ version of the text, or just the original text to not change anything.
+ '''
+ raise NotImplementedError()
+
+ def ModifyEachTextPart(self, lang, translation):
+ '''Call this to easily modify one or more of the textual parts of a
+ translation. It will call ModifyTextPart for each part of the
+ translation.
+ '''
+ contents = translation.GetContent()
+ for ix in range(len(contents)):
+ if (isinstance(contents[ix], six.string_types)):
+ contents[ix] = self.ModifyTextPart(lang, contents[ix])
+
+
+class OneOffCustomType(CustomType):
+ '''A very simple custom type that performs the validation expressed by
+ the input expression on all languages including the source language.
+ The expression can access the variables 'lang', 'msg' and 'text()' where
+ 'lang' is the language of 'msg', 'msg' is the message or translation being
+ validated and 'text()' returns the real contents of 'msg' (for shorthand).
+ '''
+ def __init__(self, expression):
+ self.expr = expression
+ def Validate(self, message):
+ return self.ValidateAndModify(MessageClique.source_language, message)
+ def ValidateAndModify(self, lang, msg):
+ def text():
+ return msg.GetRealContent()
+ return eval(self.expr, {},
+ {'lang' : lang,
+ 'text' : text,
+ 'msg' : msg,
+ })
+
+
+class MessageClique(object):
+ '''A message along with all of its translations. Also code to bring
+ translations together with their original message.'''
+
+ # change this to the language code of Messages you add to cliques_.
+ # TODO(joi) Actually change this based on the <grit> node's source language
+ source_language = 'en'
+
+ # A constant translation we use when asked for a translation into the
+ # special language constants.CONSTANT_LANGUAGE.
+ CONSTANT_TRANSLATION = tclib.Translation(text='TTTTTT')
+
+ # A pattern to match messages that are empty or whitespace only.
+ WHITESPACE_MESSAGE = lazy_re.compile(r'^\s*$')
+
+ def __init__(self, uber_clique, message, translateable=True,
+ custom_type=None):
+ '''Create a new clique initialized with just a message.
+
+ Note that messages with a body comprised only of whitespace will implicitly
+ be marked non-translatable.
+
+ Args:
+ uber_clique: Our uber-clique (collection of cliques)
+ message: tclib.Message()
+ translateable: True | False
+ custom_type: instance of clique.CustomType interface
+ '''
+ # Our parent
+ self.uber_clique = uber_clique
+ # If not translateable, we only store the original message.
+ self.translateable = translateable
+
+ # We implicitly mark messages that have a whitespace-only body as
+ # non-translateable.
+ if MessageClique.WHITESPACE_MESSAGE.match(message.GetRealContent()):
+ self.translateable = False
+
+ # A mapping of language identifiers to tclib.BaseMessage and its
+ # subclasses (i.e. tclib.Message and tclib.Translation).
+ self.clique = { MessageClique.source_language : message }
+ # A list of the "shortcut groups" this clique is
+ # part of. Within any given shortcut group, no shortcut key (e.g. &J)
+ # must appear more than once in each language for all cliques that
+ # belong to the group.
+ self.shortcut_groups = []
+ # An instance of the CustomType interface, or None. If this is set, it will
+ # be used to validate the original message and translations thereof, and
+ # will also get a chance to modify translations of the message.
+ self.SetCustomType(custom_type)
+
+ def GetMessage(self):
+ '''Retrieves the tclib.Message that is the source for this clique.'''
+ return self.clique[MessageClique.source_language]
+
+ def GetId(self):
+ '''Retrieves the message ID of the messages in this clique.'''
+ return self.GetMessage().GetId()
+
+ def IsTranslateable(self):
+ return self.translateable
+
+ def AddToShortcutGroup(self, group):
+ self.shortcut_groups.append(group)
+
+ def SetCustomType(self, custom_type):
+ '''Makes this clique use custom_type for validating messages and
+ translations, and optionally modifying translations.
+ '''
+ self.custom_type = custom_type
+ if custom_type and not custom_type.Validate(self.GetMessage()):
+ raise exception.InvalidMessage(self.GetMessage().GetRealContent())
+
+ def MessageForLanguage(self, lang, pseudo_if_no_match=True,
+ fallback_to_english=False):
+ '''Returns the message/translation for the specified language, providing
+ a pseudotranslation if there is no available translation and a pseudo-
+ translation is requested.
+
+ The translation of any message whatsoever in the special language
+ 'x_constant' is the message "TTTTTT".
+
+ Args:
+ lang: 'en'
+ pseudo_if_no_match: True
+ fallback_to_english: False
+
+ Return:
+ tclib.BaseMessage
+ '''
+ if not self.translateable:
+ return self.GetMessage()
+
+ if lang == constants.CONSTANT_LANGUAGE:
+ return self.CONSTANT_TRANSLATION
+
+ for msglang in self.clique:
+ if lang == msglang:
+ return self.clique[msglang]
+
+ if lang == constants.FAKE_BIDI:
+ return pseudo_rtl.PseudoRTLMessage(self.GetMessage())
+
+ if fallback_to_english:
+ self.uber_clique._AddMissingTranslation(lang, self, is_error=False)
+ return self.GetMessage()
+
+ # If we're not supposed to generate pseudotranslations, we add an error
+ # report to a list of errors, then fail at a higher level, so that we
+ # get a list of all messages that are missing translations.
+ if not pseudo_if_no_match:
+ self.uber_clique._AddMissingTranslation(lang, self, is_error=True)
+
+ return pseudo.PseudoMessage(self.GetMessage())
+
+ def AllMessagesThatMatch(self, lang_re, include_pseudo = True):
+ '''Returns a map of all messages that match 'lang', including the pseudo
+ translation if requested.
+
+ Args:
+ lang_re: re.compile(r'fr|en')
+ include_pseudo: True
+
+ Return:
+ { 'en' : tclib.Message,
+ 'fr' : tclib.Translation,
+ pseudo.PSEUDO_LANG : tclib.Translation }
+ '''
+ if not self.translateable:
+ return [self.GetMessage()]
+
+ matches = {}
+ for msglang in self.clique:
+ if lang_re.match(msglang):
+ matches[msglang] = self.clique[msglang]
+
+ if include_pseudo:
+ matches[pseudo.PSEUDO_LANG] = pseudo.PseudoMessage(self.GetMessage())
+
+ return matches
+
+ def AddTranslation(self, translation, language):
+ '''Add a translation to this clique. The translation must have the same
+ ID as the message that is the source for this clique.
+
+ If this clique is not translateable, the function just returns.
+
+ Args:
+ translation: tclib.Translation()
+ language: 'en'
+
+ Throws:
+ grit.exception.InvalidTranslation if the translation you're trying to add
+ doesn't have the same message ID as the source message of this clique.
+ '''
+ if not self.translateable:
+ return
+ if translation.GetId() != self.GetId():
+ raise exception.InvalidTranslation(
+ 'Msg ID %s, transl ID %s' % (self.GetId(), translation.GetId()))
+
+ assert not language in self.clique
+
+ # Because two messages can differ in the original content of their
+ # placeholders yet share the same ID (because they are otherwise the
+ # same), the translation we are getting may have different original
+ # content for placeholders than our message, yet it is still the right
+ # translation for our message (because it is for the same ID). We must
+ # therefore fetch the original content of placeholders from our original
+ # English message.
+ #
+ # See grit.clique_unittest.MessageCliqueUnittest.testSemiIdenticalCliques
+ # for a concrete explanation of why this is necessary.
+
+ original = self.MessageForLanguage(self.source_language, False)
+ if len(original.GetPlaceholders()) != len(translation.GetPlaceholders()):
+ print("ERROR: '%s' translation of message id %s does not match" %
+ (language, translation.GetId()))
+ assert False
+
+ transl_msg = tclib.Translation(id=self.GetId(),
+ text=translation.GetPresentableContent(),
+ placeholders=original.GetPlaceholders())
+
+ if (self.custom_type and
+ not self.custom_type.ValidateAndModify(language, transl_msg)):
+ print("WARNING: %s translation failed validation: %s" %
+ (language, transl_msg.GetId()))
+
+ self.clique[language] = transl_msg
diff --git a/tools/grit/grit/clique_unittest.py b/tools/grit/grit/clique_unittest.p y
new file mode 100644
index 0000000000..7d2d7318ba
--- /dev/null
+++ b/tools/grit/grit/clique_unittest.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.clique'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import re
+import unittest
+
+from six import StringIO
+
+from grit import clique
+from grit import exception
+from grit import pseudo
+from grit import tclib
+from grit import grd_reader
+from grit import util
+
+class MessageCliqueUnittest(unittest.TestCase):
+ def testClique(self):
+ factory = clique.UberClique()
+ msg = tclib.Message(text='Hello USERNAME, how are you?',
+ placeholders=[
+ tclib.Placeholder('USERNAME', '%s', 'Joi')])
+ c = factory.MakeClique(msg)
+
+ self.failUnless(c.GetMessage() == msg)
+ self.failUnless(c.GetId() == msg.GetId())
+
+ msg_fr = tclib.Translation(text='Bonjour USERNAME, comment ca va?',
+ id=msg.GetId(), placeholders=[
+ tclib.Placeholder('USERNAME', '%s', 'Joi')])
+ msg_de = tclib.Translation(text='Guten tag USERNAME, wie geht es dir?',
+ id=msg.GetId(), placeholders=[
+ tclib.Placeholder('USERNAME', '%s', 'Joi')])
+
+ c.AddTranslation(msg_fr, 'fr')
+ factory.FindCliqueAndAddTranslation(msg_de, 'de')
+
+ # sort() sorts lists in-place and does not return them
+ for lang in ('en', 'fr', 'de'):
+ self.failUnless(lang in c.clique)
+
+ self.failUnless(c.MessageForLanguage('fr').GetRealContent() ==
+ msg_fr.GetRealContent())
+
+ try:
+ c.MessageForLanguage('zh-CN', False)
+ self.fail('Should have gotten exception')
+ except:
+ pass
+
+ self.failUnless(c.MessageForLanguage('zh-CN', True) != None)
+
+ rex = re.compile('fr|de|bingo')
+ self.failUnless(len(c.AllMessagesThatMatch(rex, False)) == 2)
+ self.failUnless(
+ c.AllMessagesThatMatch(rex, True)[pseudo.PSEUDO_LANG] is not None)
+
+ def testBestClique(self):
+ factory = clique.UberClique()
+ factory.MakeClique(tclib.Message(text='Alfur', description='alfaholl'))
+ factory.MakeClique(tclib.Message(text='Alfur', description=''))
+ factory.MakeClique(tclib.Message(text='Vaettur', description=''))
+ factory.MakeClique(tclib.Message(text='Vaettur', description=''))
+ factory.MakeClique(tclib.Message(text='Troll', description=''))
+ factory.MakeClique(tclib.Message(text='Gryla', description='ID: IDS_GRYLA'))
+ factory.MakeClique(tclib.Message(text='Gryla', description='vondakerling'))
+ factory.MakeClique(tclib.Message(text='Leppaludi', description='ID: IDS_LL'))
+ factory.MakeClique(tclib.Message(text='Leppaludi', description=''))
+
+ count_best_cliques = 0
+ for c in factory.BestCliquePerId():
+ count_best_cliques += 1
+ msg = c.GetMessage()
+ text = msg.GetRealContent()
+ description = msg.GetDescription()
+ if text == 'Alfur':
+ self.failUnless(description == 'alfaholl')
+ elif text == 'Gryla':
+ self.failUnless(description == 'vondakerling')
+ elif text == 'Leppaludi':
+ self.failUnless(description == 'ID: IDS_LL')
+ self.failUnless(count_best_cliques == 5)
+
+ def testAllInUberClique(self):
+ resources = grd_reader.Parse(
+ StringIO(u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+ <release seq="3">
+ <messages>
+ <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+ Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+ </message>
+ </messages>
+ <structures>
+ <structure type="dialog" name="IDD_ABOUTBOX" encoding="utf-16" file="grit/testdata/klonk.rc" />
+ <structure type="tr_html" name="ID_HTML" file="grit/testdata/simple.html" />
+ </structures>
+ </release>
+</grit>'''), util.PathFromRoot('.'))
+ resources.SetOutputLanguage('en')
+ resources.RunGatherers()
+ content_list = []
+ for clique_list in resources.UberClique().cliques_.values():
+ for clique in clique_list:
+ content_list.append(clique.GetMessage().GetRealContent())
+ self.failUnless('Hello %s, how are you doing today?' in content_list)
+ self.failUnless('Jack "Black" Daniels' in content_list)
+ self.failUnless('Hello!' in content_list)
+
+ def testCorrectExceptionIfWrongEncodingOnResourceFile(self):
+ '''This doesn't really belong in this unittest file, but what the heck.'''
+ resources = grd_reader.Parse(
+ StringIO(u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+ <release seq="3">
+ <structures>
+ <structure type="dialog" name="IDD_ABOUTBOX" file="grit/testdata/klonk.rc" />
+ </structures>
+ </release>
+</grit>'''), util.PathFromRoot('.'))
+ self.assertRaises(exception.SectionNotFound, resources.RunGatherers)
+
+ def testSemiIdenticalCliques(self):
+ messages = [
+ tclib.Message(text='Hello USERNAME',
+ placeholders=[tclib.Placeholder('USERNAME', '$1', 'Joi')]),
+ tclib.Message(text='Hello USERNAME',
+ placeholders=[tclib.Placeholder('USERNAME', '%s', 'Joi')]),
+ ]
+ self.failUnless(messages[0].GetId() == messages[1].GetId())
+
+ # Both of the above would share a translation.
+ translation = tclib.Translation(id=messages[0].GetId(),
+ text='Bonjour USERNAME',
+ placeholders=[tclib.Placeholder(
+ 'USERNAME', '$1', 'Joi')])
+
+ factory = clique.UberClique()
+ cliques = [factory.MakeClique(msg) for msg in messages]
+
+ for clq in cliques:
+ clq.AddTranslation(translation, 'fr')
+
+ self.failUnless(cliques[0].MessageForLanguage('fr').GetRealContent() ==
+ 'Bonjour $1')
+ self.failUnless(cliques[1].MessageForLanguage('fr').GetRealContent() ==
+ 'Bonjour %s')
+
+ def testMissingTranslations(self):
+ messages = [ tclib.Message(text='Hello'), tclib.Message(text='Goodbye') ]
+ factory = clique.UberClique()
+ cliques = [factory.MakeClique(msg) for msg in messages]
+
+ cliques[1].MessageForLanguage('fr', False, True)
+
+ self.failUnless(not factory.HasMissingTranslations())
+
+ cliques[0].MessageForLanguage('de', False, False)
+
+ self.failUnless(factory.HasMissingTranslations())
+
+ report = factory.MissingTranslationsReport()
+ self.failUnless(report.count('WARNING') == 1)
+ self.failUnless(report.count('8053599568341804890 "Goodbye" fr') == 1)
+ self.failUnless(report.count('ERROR') == 1)
+ self.failUnless(report.count('800120468867715734 "Hello" de') == 1)
+
+ def testCustomTypes(self):
+ factory = clique.UberClique()
+ message = tclib.Message(text='Bingo bongo')
+ c = factory.MakeClique(message)
+ try:
+ c.SetCustomType(DummyCustomType())
+ self.fail()
+ except:
+ pass # expected case - 'Bingo bongo' does not start with 'jjj'
+
+ message = tclib.Message(text='jjjBingo bongo')
+ c = factory.MakeClique(message)
+ c.SetCustomType(util.NewClassInstance(
+ 'grit.clique_unittest.DummyCustomType', clique.CustomType))
+ translation = tclib.Translation(id=message.GetId(), text='Bilingo bolongo')
+ c.AddTranslation(translation, 'fr')
+ self.failUnless(c.MessageForLanguage('fr').GetRealContent().startswith('jjj'))
+
+ def testWhitespaceMessagesAreNontranslateable(self):
+ factory = clique.UberClique()
+
+ message = tclib.Message(text=' \t')
+ c = factory.MakeClique(message, translateable=True)
+ self.failIf(c.IsTranslateable())
+
+ message = tclib.Message(text='\n \n ')
+ c = factory.MakeClique(message, translateable=True)
+ self.failIf(c.IsTranslateable())
+
+ message = tclib.Message(text='\n hello')
+ c = factory.MakeClique(message, translateable=True)
+ self.failUnless(c.IsTranslateable())
+
+ def testEachCliqueKeptSorted(self):
+ factory = clique.UberClique()
+ msg_a = tclib.Message(text='hello', description='a')
+ msg_b = tclib.Message(text='hello', description='b')
+ msg_c = tclib.Message(text='hello', description='c')
+ # Insert out of order
+ clique_b = factory.MakeClique(msg_b, translateable=True)
+ clique_a = factory.MakeClique(msg_a, translateable=True)
+ clique_c = factory.MakeClique(msg_c, translateable=True)
+ clique_list = factory.cliques_[clique_a.GetId()]
+ self.failUnless(len(clique_list) == 3)
+ self.failUnless(clique_list[0] == clique_a)
+ self.failUnless(clique_list[1] == clique_b)
+ self.failUnless(clique_list[2] == clique_c)
+
+ def testBestCliqueSortIsStable(self):
+ factory = clique.UberClique()
+ text = 'hello'
+ msg_no_description = tclib.Message(text=text)
+ msg_id_description_a = tclib.Message(text=text, description='ID: a')
+ msg_id_description_b = tclib.Message(text=text, description='ID: b')
+ msg_description_x = tclib.Message(text=text, description='x')
+ msg_description_y = tclib.Message(text=text, description='y')
+ clique_id = msg_no_description.GetId()
+
+ # Insert in an order that tests all outcomes.
+ clique_no_description = factory.MakeClique(msg_no_description,
+ translateable=True)
+ self.failUnless(factory.BestClique(clique_id) == clique_no_description)
+ clique_id_description_b = factory.MakeClique(msg_id_description_b,
+ translateable=True)
+ self.failUnless(factory.BestClique(clique_id) == clique_id_description_b)
+ clique_id_description_a = factory.MakeClique(msg_id_description_a,
+ translateable=True)
+ self.failUnless(factory.BestClique(clique_id) == clique_id_description_a)
+ clique_description_y = factory.MakeClique(msg_description_y,
+ translateable=True)
+ self.failUnless(factory.BestClique(clique_id) == clique_description_y)
+ clique_description_x = factory.MakeClique(msg_description_x,
+ translateable=True)
+ self.failUnless(factory.BestClique(clique_id) == clique_description_x)
+
+
+class DummyCustomType(clique.CustomType):
+ def Validate(self, message):
+ return message.GetRealContent().startswith('jjj')
+ def ValidateAndModify(self, lang, translation):
+ is_ok = self.Validate(translation)
+ self.ModifyEachTextPart(lang, translation)
+ def ModifyTextPart(self, lang, text):
+ return 'jjj%s' % text
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/constants.py b/tools/grit/grit/constants.py
new file mode 100644
index 0000000000..8229c94b09
--- /dev/null
+++ b/tools/grit/grit/constants.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Constant definitions for GRIT.
+'''
+
+from __future__ import print_function
+
+# This is the Icelandic noun meaning "grit" and is used to check that our
+# input files are in the correct encoding. The middle character gets encoded
+# as two bytes in UTF-8, so this is sufficient to detect incorrect encoding.
+ENCODING_CHECK = u'm\u00f6l'
+
+# A special language, translations into which are always "TTTTTT".
+CONSTANT_LANGUAGE = 'x_constant'
+
+FAKE_BIDI = 'fake-bidi'
+
+# Magic number added to the header of resources brotli compressed by grit. Used
+# to easily identify resources as being brotli compressed. See
+# ui/base/resource/resource_bundle.h for decompression usage.
+BROTLI_CONST = b'\x1e\x9b'
diff --git a/tools/grit/grit/exception.py b/tools/grit/grit/exception.py
new file mode 100644
index 0000000000..2a363fb077
--- /dev/null
+++ b/tools/grit/grit/exception.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Exception types for GRIT.
+'''
+
+from __future__ import print_function
+
+class Base(Exception):
+ '''A base exception that uses the class's docstring in addition to any
+ user-provided message as the body of the Base.
+ '''
+ def __init__(self, msg=''):
+ if len(msg):
+ if self.__doc__:
+ msg = self.__doc__ + ': ' + msg
+ else:
+ msg = self.__doc__
+ super(Base, self).__init__(msg)
+
+
+class Parsing(Base):
+ '''An error occurred parsing a GRD or XTB file.'''
+ pass
+
+
+class UnknownElement(Parsing):
+ '''An unknown node type was encountered.'''
+ pass
+
+
+class MissingElement(Parsing):
+ '''An expected element was missing.'''
+ pass
+
+
+class UnexpectedChild(Parsing):
+ '''An unexpected child element was encountered (on a leaf node).'''
+ pass
+
+
+class UnexpectedAttribute(Parsing):
+ '''The attribute was not expected'''
+ pass
+
+
+class UnexpectedContent(Parsing):
+ '''This element should not have content'''
+ pass
+
+class MissingMandatoryAttribute(Parsing):
+ '''This element is missing a mandatory attribute'''
+ pass
+
+
+class MutuallyExclusiveMandatoryAttribute(Parsing):
+ '''This element has 2 mutually exclusive mandatory attributes'''
+ pass
+
+
+class DuplicateKey(Parsing):
+ '''A duplicate key attribute was found.'''
+ pass
+
+
+class TooManyExamples(Parsing):
+ '''Only one <ex> element is allowed for each <ph> element.'''
+ pass
+
+
+class FileNotFound(Parsing):
+ '''The resource file was not found.'''
+ pass
+
+
+class InvalidMessage(Base):
+ '''The specified message failed validation.'''
+ pass
+
+
+class InvalidTranslation(Base):
+ '''Attempt to add an invalid translation to a clique.'''
+ pass
+
+
+class NoSuchTranslation(Base):
+ '''Requested translation not available'''
+ pass
+
+
+class NotReady(Base):
+ '''Attempt to use an object before it is ready, or attempt to translate \
+an empty document.'''
+ pass
+
+
+class MismatchingPlaceholders(Base):
+ '''Placeholders do not match.'''
+ pass
+
+
+class InvalidPlaceholderName(Base):
+ '''Placeholder name can only contain A-Z, a-z, 0-9 and underscore.'''
+ pass
+
+
+class BlockTagInTranslateableChunk(Base):
+ '''A block tag was encountered where it wasn't expected.'''
+ pass
+
+
+class SectionNotFound(Base):
+ '''The section you requested was not found in the RC file. Make \
+sure the section ID is correct (matches the section's ID in the RC file). \
+Also note that you may need to specify the RC file's encoding (using the \
+encoding="" attribute) if it is not in the default Windows-1252 encoding. \
+'''
+ pass
+
+
+class IdRangeOverlap(Base):
+ '''ID range overlap.'''
+ pass
+
+
+class ReservedHeaderCollision(Base):
+ '''Resource included with first 3 bytes matching reserved header.'''
+ pass
+
+
+class PlaceholderNotInsidePhNode(Base):
+ '''Placeholder formatters should be inside <ph> element.'''
+ pass
+
+
+class InvalidCharactersInsidePhNode(Base):
+ '''Invalid characters found inside <ph> element.'''
+ pass
diff --git a/tools/grit/grit/extern/BogoFP.py b/tools/grit/grit/extern/BogoFP.py
new file mode 100644
index 0000000000..fc90145833
--- /dev/null
+++ b/tools/grit/grit/extern/BogoFP.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Bogus fingerprint implementation, do not use for production,
+provided only as an example.
+
+Usage:
+ grit.py -h grit.extern.BogoFP xmb /tmp/foo
+"""
+
+from __future__ import print_function
+
+import grit.extern.FP
+
+
+def UnsignedFingerPrint(str, encoding='utf-8'):
+ """Generate a fingerprint not intended for production from str (it
+ reduces the precision of the production fingerprint by one bit).
+ """
+ return (0xFFFFF7FFFFFFFFFF &
+ grit.extern.FP._UnsignedFingerPrintImpl(str, encoding))
diff --git a/tools/grit/grit/extern/FP.py b/tools/grit/grit/extern/FP.py
new file mode 100644
index 0000000000..f4ec4d943f
--- /dev/null
+++ b/tools/grit/grit/extern/FP.py
@@ -0,0 +1,72 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import print_function
+
+try:
+ import hashlib
+ _new_md5 = hashlib.md5
+except ImportError:
+ import md5
+ _new_md5 = md5.new
+
+
+"""64-bit fingerprint support for strings.
+
+Usage:
+ from extern import FP
+ print('Fingerprint is %ld' % FP.FingerPrint('Hello world!'))
+"""
+
+
+def _UnsignedFingerPrintImpl(str, encoding='utf-8'):
+ """Generate a 64-bit fingerprint by taking the first half of the md5
+ of the string.
+ """
+ hex128 = _new_md5(str.encode(encoding)).hexdigest()
+ int64 = int(hex128[:16], 16)
+ return int64
+
+
+def UnsignedFingerPrint(str, encoding='utf-8'):
+ """Generate a 64-bit fingerprint.
+
+ The default implementation uses _UnsignedFingerPrintImpl, which
+ takes the first half of the md5 of the string, but the
+ implementation may be switched using SetUnsignedFingerPrintImpl.
+ """
+ return _UnsignedFingerPrintImpl(str, encoding)
+
+
+def FingerPrint(str, encoding='utf-8'):
+ fp = UnsignedFingerPrint(str, encoding=encoding)
+ # interpret fingerprint as signed longs
+ if fp & 0x8000000000000000:
+ fp = -((~fp & 0xFFFFFFFFFFFFFFFF) + 1)
+ return fp
+
+
+def UseUnsignedFingerPrintFromModule(module_name):
+ """Imports module_name and replaces UnsignedFingerPrint in the
+ current module with the function of the same name from the imported
+ module.
+
+ Returns the function object previously known as
+ grit.extern.FP.UnsignedFingerPrint.
+ """
+ hash_module = __import__(module_name, fromlist=[module_name])
+ return SetUnsignedFingerPrint(hash_module.UnsignedFingerPrint)
+
+
+def SetUnsignedFingerPrint(function_object):
+ """Sets grit.extern.FP.UnsignedFingerPrint to point to
+ function_object.
+
+ Returns the function object previously known as
+ grit.extern.FP.UnsignedFingerPrint.
+ """
+ global UnsignedFingerPrint
+ original_function_object = UnsignedFingerPrint
+ UnsignedFingerPrint = function_object
+ return original_function_object
diff --git a/tools/grit/grit/extern/__init__.py b/tools/grit/grit/extern/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tools/grit/grit/extern/tclib.py b/tools/grit/grit/extern/tclib.py
new file mode 100644
index 0000000000..9952a87c11
--- /dev/null
+++ b/tools/grit/grit/extern/tclib.py
@@ -0,0 +1,503 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# The tclib module contains tools for aggregating, verifying, and storing
+# messages destined for the Translation Console, as well as for reading
+# translations back and outputting them in some desired format.
+#
+# This has been stripped down to include only the functionality needed by grit
+# for creating Windows .rc and .h files. These are the only parts needed by
+# the Chrome build process.
+
+from __future__ import print_function
+
+from grit.extern import FP
+
+# This module assumes that within a bundle no two messages can have the
+# same id unless they're identical.
+
+# The basic classes defined here for external use are Message and Translation,
+# where the former is used for English messages and the latter for
+# translations. These classes have a lot of common functionality, as expressed
+# by the common parent class BaseMessage. Perhaps the most important
+# distinction is that translated text is stored in UTF-8, whereas original text
+# is stored in whatever encoding the client uses (presumably Latin-1).
+
+# --------------------
+# The public interface
+# --------------------
+
+# Generate message id from message text and meaning string (optional),
+# both in utf-8 encoding
+#
+def GenerateMessageId(message, meaning=''):
+ fp = FP.FingerPrint(message)
+ if meaning:
+ # combine the fingerprints of message and meaning
+ fp2 = FP.FingerPrint(meaning)
+ if fp < 0:
+ fp = fp2 + (fp << 1) + 1
+ else:
+ fp = fp2 + (fp << 1)
+ # To avoid negative ids we strip the high-order bit
+ return str(fp & 0x7fffffffffffffff)
+
+# -------------------------------------------------------------------------
+# The MessageTranslationError class is used to signal tclib-specific errors.
+
+
+class MessageTranslationError(Exception):
+
+ def __init__(self, args = ''):
+ self.args = args
+
+
+# -----------------------------------------------------------
+# The Placeholder class represents a placeholder in a message.
+
+class Placeholder(object):
+ # String representation
+ def __str__(self):
+ return '%s, "%s", "%s"' % \
+ (self.__presentation, self.__original, self.__example)
+
+ # Getters
+ def GetOriginal(self):
+ return self.__original
+
+ def GetPresentation(self):
+ return self.__presentation
+
+ def GetExample(self):
+ return self.__example
+
+ def __eq__(self, other):
+ return self.EqualTo(other, strict=1, ignore_trailing_spaces=0)
+
+ # Equality test
+ #
+ # ignore_trailing_spaces: TC is using varchar to store the
+ # phrwr fields, as a result of that, the trailing spaces
+ # are removed by MySQL when the strings are stored into TC:-(
+ # ignore_trailing_spaces parameter is used to ignore
+ # trailing spaces during equivalence comparison.
+ #
+ def EqualTo(self, other, strict = 1, ignore_trailing_spaces = 1):
+ if type(other) is not Placeholder:
+ return 0
+ if StringEquals(self.__presentation, other.__presentation,
+ ignore_trailing_spaces):
+ if not strict or (StringEquals(self.__original, other.__original,
+ ignore_trailing_spaces) and
+ StringEquals(self.__example, other.__example,
+ ignore_trailing_spaces)):
+ return 1
+ return 0
+
+
+# -----------------------------------------------------------------
+# BaseMessage is the common parent class of Message and Translation.
+# It is not meant for direct use.
+
+class BaseMessage(object):
+ # Three types of message construction is supported. If the message text is a
+ # simple string with no dynamic content, you can pass it to the constructor
+ # as the "text" parameter. Otherwise, you can omit "text" and assemble the
+ # message step by step using AppendText() and AppendPlaceholder(). Or, as an
+ # alternative, you can give the constructor the "presentable" version of the
+ # message and a list of placeholders; it will then parse the presentation and
+ # build the message accordingly. For example:
+ # Message(text = "There are NUM_BUGS bugs in your code",
+ # placeholders = [Placeholder("NUM_BUGS", "%d", "33")],
+ # description = "Bla bla bla")
+ def __eq__(self, other):
+ # "source encoding" is nonsense, so ignore it
+ return _ObjectEquals(self, other, ['_BaseMessage__source_encoding'])
+
+ def GetName(self):
+ return self.__name
+
+ def GetSourceEncoding(self):
+ return self.__source_encoding
+
+ # Append a placeholder to the message
+ def AppendPlaceholder(self, placeholder):
+ if not isinstance(placeholder, Placeholder):
+ raise MessageTranslationError("Invalid message placeholder %s in "
+ "message %s" % (placeholder, self.GetId()))
+ # Are there other placeholders with the same presentation?
+ # If so, they need to be the same.
+ for other in self.GetPlaceholders():
+ if placeholder.GetPresentation() == other.GetPresentation():
+ if not placeholder.EqualTo(other):
+ raise MessageTranslationError(
+ "Conflicting declarations of %s within message" %
+ placeholder.GetPresentation())
+ # update placeholder list
+ dup = 0
+ for item in self.__content:
+ if isinstance(item, Placeholder) and placeholder.EqualTo(item):
+ dup = 1
+ break
+ if not dup:
+ self.__placeholders.append(placeholder)
+
+ # update content
+ self.__content.append(placeholder)
+
+ # Strips leading and trailing whitespace, and returns a tuple
+ # containing the leading and trailing space that was removed.
+ def Strip(self):
+ leading = trailing = ''
+ if len(self.__content) > 0:
+ s0 = self.__content[0]
+ if not isinstance(s0, Placeholder):
+ s = s0.lstrip()
+ leading = s0[:-len(s)]
+ self.__content[0] = s
+
+ s0 = self.__content[-1]
+ if not isinstance(s0, Placeholder):
+ s = s0.rstrip()
+ trailing = s0[len(s):]
+ self.__content[-1] = s
+ return leading, trailing
+
+ # Return the id of this message
+ def GetId(self):
+ if self.__id is None:
+ return self.GenerateId()
+ return self.__id
+
+ # Set the id of this message
+ def SetId(self, id):
+ if id is None:
+ self.__id = None
+ else:
+ self.__id = str(id) # Treat numerical ids as strings
+
+ # Return content of this message as a list (internal use only)
+ def GetContent(self):
+ return self.__content
+
+ # Return a human-readable version of this message
+ def GetPresentableContent(self):
+ presentable_content = ""
+ for item in self.__content:
+ if isinstance(item, Placeholder):
+ presentable_content += item.GetPresentation()
+ else:
+ presentable_content += item
+
+ return presentable_content
+
+ # Return a fragment of a message in escaped format
+ def EscapeFragment(self, fragment):
+ return fragment.replace('%', '%%')
+
+ # Return the "original" version of this message, doing %-escaping
+ # properly. If source_msg is specified, the placeholder original
+ # information inside source_msg will be used instead.
+ def GetOriginalContent(self, source_msg = None):
+ original_content = ""
+ for item in self.__content:
+ if isinstance(item, Placeholder):
+ if source_msg:
+ ph = source_msg.GetPlaceholder(item.GetPresentation())
+ if not ph:
+ raise MessageTranslationError(
+ "Placeholder %s doesn't exist in message: %s" %
+ (item.GetPresentation(), source_msg))
+ original_content += ph.GetOriginal()
+ else:
+ original_content += item.GetOriginal()
+ else:
+ original_content += self.EscapeFragment(item)
+ return original_content
+
+ # Return the example of this message
+ def GetExampleContent(self):
+ example_content = ""
+ for item in self.__content:
+ if isinstance(item, Placeholder):
+ example_content += item.GetExample()
+ else:
+ example_content += item
+ return example_content
+
+ # Return a list of all unique placeholders in this message
+ def GetPlaceholders(self):
+ return self.__placeholders
+
+ # Return a placeholder in this message
+ def GetPlaceholder(self, presentation):
+ for item in self.__content:
+ if (isinstance(item, Placeholder) and
+ item.GetPresentation() == presentation):
+ return item
+ return None
+
+ # Return this message's description
+ def GetDescription(self):
+ return self.__description
+
+ # Add a message source
+ def AddSource(self, source):
+ self.__sources.append(source)
+
+ # Return this message's sources as a list
+ def GetSources(self):
+ return self.__sources
+
+ # Return this message's sources as a string
+ def GetSourcesAsText(self, delimiter = "; "):
+ return delimiter.join(self.__sources)
+
+ # Set the obsolete flag for a message (internal use only)
+ def SetObsolete(self):
+ self.__obsolete = 1
+
+ # Get the obsolete flag for a message (internal use only)
+ def IsObsolete(self):
+ return self.__obsolete
+
+ # Get the sequence number (0 by default)
+ def GetSequenceNumber(self):
+ return self.__sequence_number
+
+ # Set the sequence number
+ def SetSequenceNumber(self, number):
+ self.__sequence_number = number
+
+ # Increment instance counter
+ def AddInstance(self):
+ self.__num_instances += 1
+
+ # Return instance count
+ def GetNumInstances(self):
+ return self.__num_instances
+
+ def GetErrors(self, from_tc=0):
+ """
+ Returns a description of the problem if the message is not
+ syntactically valid, or None if everything is fine.
+
+ Args:
+ from_tc: indicates whether this message came from the TC. We let
+ the TC get away with some things we normally wouldn't allow for
+ historical reasons.
+ """
+ # check that placeholders are unambiguous
+ pos = 0
+ phs = {}
+ for item in self.__content:
+ if isinstance(item, Placeholder):
+ phs[pos] = item
+ pos += len(item.GetPresentation())
+ else:
+ pos += len(item)
+ presentation = self.GetPresentableContent()
+ for ph in self.GetPlaceholders():
+ for pos in FindOverlapping(presentation, ph.GetPresentation()):
+ # message contains the same text as a placeholder presentation
+ other_ph = phs.get(pos)
+ if ((not other_ph
+ and not IsSubstringInPlaceholder(pos, len(ph.GetPresentation()), phs))
+ or
+ (other_ph and len(other_ph.GetPresentation()) < len(ph.GetPresentation()))):
+ return "message contains placeholder name '%s':\n%s" % (
+ ph.GetPresentation(), presentation)
+ return None
+
+
+ def __CopyTo(self, other):
+ """
+ Returns a copy of this BaseMessage.
+ """
+ assert isinstance(other, self.__class__) or isinstance(self, other.__class__)
+ other.__source_encoding = self.__source_encoding
+ other.__content = self.__content[:]
+ other.__description = self.__description
+ other.__id = self.__id
+ other.__num_instances = self.__num_instances
+ other.__obsolete = self.__obsolete
+ other.__name = self.__name
+ other.__placeholders = self.__placeholders[:]
+ other.__sequence_number = self.__sequence_number
+ other.__sources = self.__sources[:]
+
+ return other
+
+ def HasText(self):
+ """Returns true iff this message has anything other than placeholders."""
+ for item in self.__content:
+ if not isinstance(item, Placeholder):
+ return True
+ return False
+
+# --------------------------------------------------------
+# The Message class represents original (English) messages
+
+class Message(BaseMessage):
+ # See BaseMessage constructor
+ def __init__(self, source_encoding, text=None, id=None,
+ description=None, meaning="", placeholders=None,
+ source=None, sequence_number=0, clone_from=None,
+ time_created=0, name=None, is_hidden = 0):
+
+ if clone_from is not None:
+ BaseMessage.__init__(self, None, clone_from=clone_from)
+ self.__meaning = clone_from.__meaning
+ self.__time_created = clone_from.__time_created
+ self.__is_hidden = clone_from.__is_hidden
+ return
+
+ BaseMessage.__init__(self, source_encoding, text, id, description,
+ placeholders, source, sequence_number,
+ name=name)
+ self.__meaning = meaning
+ self.__time_created = time_created
+ self.SetIsHidden(is_hidden)
+
+ # String representation
+ def __str__(self):
+ s = 'source: %s, id: %s, content: "%s", meaning: "%s", ' \
+ 'description: "%s"' % \
+ (self.GetSourcesAsText(), self.GetId(), self.GetPresentableContent(),
+ self.__meaning, self.GetDescription())
+ if self.GetName() is not None:
+ s += ', name: "%s"' % self.GetName()
+ placeholders = self.GetPlaceholders()
+ for i in range(len(placeholders)):
+ s += ", placeholder[%d]: %s" % (i, placeholders[i])
+ return s
+
+ # Strips leading and trailing whitespace, and returns a tuple
+ # containing the leading and trailing space that was removed.
+ def Strip(self):
+ leading = trailing = ''
+ content = self.GetContent()
+ if len(content) > 0:
+ s0 = content[0]
+ if not isinstance(s0, Placeholder):
+ s = s0.lstrip()
+ leading = s0[:-len(s)]
+ content[0] = s
+
+ s0 = content[-1]
+ if not isinstance(s0, Placeholder):
+ s = s0.rstrip()
+ trailing = s0[len(s):]
+ content[-1] = s
+ return leading, trailing
+
+ # Generate an id by hashing message content
+ def GenerateId(self):
+ self.SetId(GenerateMessageId(self.GetPresentableContent(),
+ self.__meaning))
+ return self.GetId()
+
+ def GetMeaning(self):
+ return self.__meaning
+
+ def GetTimeCreated(self):
+ return self.__time_created
+
+ # Equality operator
+ def EqualTo(self, other, strict = 1):
+ # Check id, meaning, content
+ if self.GetId() != other.GetId():
+ return 0
+ if self.__meaning != other.__meaning:
+ return 0
+ if self.GetPresentableContent() != other.GetPresentableContent():
+ return 0
+ # Check descriptions if comparison is strict
+ if (strict and
+ self.GetDescription() is not None and
+ other.GetDescription() is not None and
+ self.GetDescription() != other.GetDescription()):
+ return 0
+ # Check placeholders
+ ph1 = self.GetPlaceholders()
+ ph2 = other.GetPlaceholders()
+ if len(ph1) != len(ph2):
+ return 0
+ for i in range(len(ph1)):
+ if not ph1[i].EqualTo(ph2[i], strict):
+ return 0
+
+ return 1
+
+ def Copy(self):
+ """
+ Returns a copy of this Message.
+ """
+ assert isinstance(self, Message)
+ return Message(None, clone_from=self)
+
+ def SetIsHidden(self, is_hidden):
+ """Sets whether this message should be hidden.
+
+ Args:
+ is_hidden : 0 or 1 - if the message should be hidden, 0 otherwise
+ """
+ if is_hidden not in [0, 1]:
+ raise MessageTranslationError("is_hidden must be 0 or 1, got %s")
+ self.__is_hidden = is_hidden
+
+ def IsHidden(self):
+ """Returns 1 if this message is hidden, and 0 otherwise."""
+ return self.__is_hidden
+
+# ----------------------------------------------------
+# The Translation class represents translated messages
+
+class Translation(BaseMessage):
+ # See BaseMessage constructor
+ def __init__(self, source_encoding, text=None, id=None,
+ description=None, placeholders=None, source=None,
+ sequence_number=0, clone_from=None, ignore_ph_errors=0,
+ name=None):
+ if clone_from is not None:
+ BaseMessage.__init__(self, None, clone_from=clone_from)
+ return
+
+ BaseMessage.__init__(self, source_encoding, text, id, description,
+ placeholders, source, sequence_number,
+ ignore_ph_errors=ignore_ph_errors, name=name)
+
+ # String representation
+ def __str__(self):
+ s = 'source: %s, id: %s, content: "%s", description: "%s"' % \
+ (self.GetSourcesAsText(), self.GetId(), self.GetPresentableContent(),
+ self.GetDescription());
+ placeholders = self.GetPlaceholders()
+ for i in range(len(placeholders)):
+ s += ", placeholder[%d]: %s" % (i, placeholders[i])
+ return s
+
+ # Equality operator
+ def EqualTo(self, other, strict=1):
+ # Check id and content
+ if self.GetId() != other.GetId():
+ return 0
+ if self.GetPresentableContent() != other.GetPresentableContent():
+ return 0
+ # Check placeholders
+ ph1 = self.GetPlaceholders()
+ ph2 = other.GetPlaceholders()
+ if len(ph1) != len(ph2):
+ return 0
+ for i in range(len(ph1)):
+ if not ph1[i].EqualTo(ph2[i], strict):
+ return 0
+
+ return 1
+
+ def Copy(self):
+ """
+ Returns a copy of this Translation.
+ """
+ return Translation(None, clone_from=self)
diff --git a/tools/grit/grit/format/__init__.py b/tools/grit/grit/format/__init__.py
new file mode 100644
index 0000000000..55d56b8cfd
--- /dev/null
+++ b/tools/grit/grit/format/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Module grit.format
+'''
+
+pass
diff --git a/tools/grit/grit/format/android_xml.py b/tools/grit/grit/format/android_xml.py
new file mode 100644
index 0000000000..7eb288891f
--- /dev/null
+++ b/tools/grit/grit/format/android_xml.py
@@ -0,0 +1,212 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Produces localized strings.xml files for Android.
+
+In cases where an "android" type output file is requested in a grd, the classes
+in android_xml will process the messages and translations to produce a valid
+strings.xml that is properly localized with the specified language.
+
+For example if the following output tag were to be included in a grd file
+ <outputs>
+ ...
+ <output filename="values-es/strings.xml" type="android" lang="es" />
+ ...
+ </outputs>
+
+for a grd file with the following messages:
+
+ <message name="IDS_HELLO" desc="Simple greeting">Hello</message>
+ <message name="IDS_WORLD" desc="The world">world</message>
+
+and there existed an appropriate xtb file containing the Spanish translations,
+then the output would be:
+
+ <?xml version="1.0" encoding="utf-8"?>
+ <resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <string name="hello">"Hola"</string>
+ <string name="world">"mundo"</string>
+ </resources>
+
+which would be written to values-es/strings.xml and usable by the Android
+resource framework.
+
+Advanced usage
+--------------
+
+To process only certain messages in a grd file, tag each desired message by
+adding "android_java" to formatter_data. Then set the environmental variable
+ANDROID_JAVA_TAGGED_ONLY to "true" when building the grd file. For example:
+
+ <message name="IDS_HELLO" formatter_data="android_java">Hello</message>
+
+To generate Android plurals (aka "quantity strings"), use the ICU plural syntax
+in the grd file. This will automatically be transformed into a <purals> element
+in the output xml file. For example:
+
+ <message name="IDS_CATS">
+ {NUM_CATS, plural,
+ =1 {1 cat}
+ other {# cats}}
+ </message>
+
+ will produce
+
+ <plurals name="cats">
+ <item quantity="one">1 Katze</item>
+ <item quantity="other">%d Katzen</item>
+ </plurals>
+"""
+
+from __future__ import print_function
+
+import os
+import re
+import xml.sax.saxutils
+
+from grit import lazy_re
+from grit.node import message
+
+
+# When this environmental variable has value "true", only tagged messages will
+# be outputted.
+_TAGGED_ONLY_ENV_VAR = 'ANDROID_JAVA_TAGGED_ONLY'
+_TAGGED_ONLY_DEFAULT = False
+
+# In tagged-only mode, only messages with this tag will be ouputted.
+_EMIT_TAG = 'android_java'
+
+_NAME_PATTERN = lazy_re.compile(r'IDS_(?P<name>[A-Z0-9_]+)\Z')
+
+# Most strings are output as a <string> element. Note the double quotes
+# around the value to preserve whitespace.
+_STRING_TEMPLATE = u'<string name="%s">"%s"</string>\n'
+
+# Some strings are output as a <plurals> element.
+_PLURALS_TEMPLATE = '<plurals name="%s">\n%s</plurals>\n'
+_PLURALS_ITEM_TEMPLATE = ' <item quantity="%s">%s</item>\n'
+
+# Matches e.g. "{HELLO, plural, HOW ARE YOU DOING}", while capturing
+# "HOW ARE YOU DOING" in <items>.
+_PLURALS_PATTERN = lazy_re.compile(r'\{[A-Z_]+,\s*plural,(?P<items>.*)\}$',
+ flags=re.S)
+
+# Repeatedly matched against the <items> capture in _PLURALS_PATTERN,
+# to match "<quantity>{<value>}".
+_PLURALS_ITEM_PATTERN = lazy_re.compile(r'(?P<quantity>\S+?)\s*'
+ r'\{(?P<value>.*?)\}')
+_PLURALS_QUANTITY_MAP = {
+ '=0': 'zero',
+ 'zero': 'zero',
+ '=1': 'one',
+ 'one': 'one',
+ '=2': 'two',
+ 'two': 'two',
+ 'few': 'few',
+ 'many': 'many',
+ 'other': 'other',
+}
+
+
+def Format(root, lang='en', output_dir='.'):
+ yield ('<?xml version="1.0" encoding="utf-8"?>\n'
+ '<resources '
+ 'xmlns:android="http://schemas.android.com/apk/res/android">\n')
+
+ tagged_only = _TAGGED_ONLY_DEFAULT
+ if _TAGGED_ONLY_ENV_VAR in os.environ:
+ tagged_only = os.environ[_TAGGED_ONLY_ENV_VAR].lower()
+ if tagged_only == 'true':
+ tagged_only = True
+ elif tagged_only == 'false':
+ tagged_only = False
+ else:
+ raise Exception('env variable ANDROID_JAVA_TAGGED_ONLY must have value '
+ 'true or false. Invalid value: %s' % tagged_only)
+
+ for item in root.ActiveDescendants():
+ with item:
+ if ShouldOutputNode(item, tagged_only):
+ yield _FormatMessage(item, lang)
+
+ yield '</resources>\n'
+
+
+def ShouldOutputNode(node, tagged_only):
+ """Returns true if node should be outputted.
+
+ Args:
+ node: a Node from the grd dom
+ tagged_only: true, if only tagged messages should be outputted
+ """
+ return (isinstance(node, message.MessageNode) and
+ (not tagged_only or _EMIT_TAG in node.formatter_data))
+
+
+def _FormatPluralMessage(message):
+ """Compiles ICU plural syntax to the body of an Android <plurals> element.
+
+ 1. In a .grd file, we can write a plural string like this:
+
+ <message name="IDS_THINGS">
+ {NUM_THINGS, plural,
+ =1 {1 thing}
+ other {# things}}
+ </message>
+
+ 2. The Android equivalent looks like this:
+
+ <plurals name="things">
+ <item quantity="one">1 thing</item>
+ <item quantity="other">%d things</item>
+ </plurals>
+
+ This method takes the body of (1) and converts it to the body of (2).
+
+ If the message is *not* a plural string, this function returns `None`.
+ If the message includes quantities without an equivalent format in Android,
+ it raises an exception.
+ """
+ ret = {}
+ plural_match = _PLURALS_PATTERN.match(message)
+ if not plural_match:
+ return None
+ body_in = plural_match.group('items').strip()
+ lines = []
+ quantities_so_far = set()
+ for item_match in _PLURALS_ITEM_PATTERN.finditer(body_in):
+ quantity_in = item_match.group('quantity')
+ quantity_out = _PLURALS_QUANTITY_MAP.get(quantity_in)
+ value_in = item_match.group('value')
+ value_out = '"' + value_in.replace('#', '%d') + '"'
+ if quantity_out:
+ # only one line per quantity out (https://crbug.com/787488)
+ if quantity_out not in quantities_so_far:
+ quantities_so_far.add(quantity_out)
+ lines.append(_PLURALS_ITEM_TEMPLATE % (quantity_out, value_out))
+ else:
+ raise Exception('Unsupported plural quantity for android '
+ 'strings.xml: %s' % quantity_in)
+ return ''.join(lines)
+
+
+def _FormatMessage(item, lang):
+ """Writes out a single string as a <resource/> element."""
+
+ mangled_name = item.GetTextualIds()[0]
+ match = _NAME_PATTERN.match(mangled_name)
+ if not match:
+ raise Exception('Unexpected resource name: %s' % mangled_name)
+ name = match.group('name').lower()
+
+ value = item.ws_at_start + item.Translate(lang) + item.ws_at_end
+ # Replace < > & with < > & to ensure we generate valid XML and
+ # replace ' " with \' \" to conform to Android's string formatting rules.
+ value = xml.sax.saxutils.escape(value, {"'": "\\'", '"': '\\"'})
+
+ plurals = _FormatPluralMessage(value)
+ if plurals:
+ return _PLURALS_TEMPLATE % (name, plurals)
+ else:
+ return _STRING_TEMPLATE % (name, value)
diff --git a/tools/grit/grit/format/android_xml_unittest.py b/tools/grit/grit/format/android_xml_unittest.py
new file mode 100644
index 0000000000..d9f476fddf
--- /dev/null
+++ b/tools/grit/grit/format/android_xml_unittest.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for android_xml.py."""
+
+from __future__ import print_function
+
+import os
+import sys
+import unittest
+
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from six import StringIO
+
+from grit import util
+from grit.format import android_xml
+from grit.node import message
+from grit.tool import build
+
+
+class AndroidXmlUnittest(unittest.TestCase):
+
+ def testMessages(self):
+ root = util.ParseGrdForUnittest(r"""
+ <messages>
+ <message name="IDS_SIMPLE" desc="A vanilla string">
+ Martha
+ </message>
+ <message name="IDS_ONE_LINE" desc="On one line">sat and wondered</message>
+ <message name="IDS_QUOTES" desc="A string with quotation marks">
+ out loud, "Why don't I build a flying car?"
+ </message>
+ <message name="IDS_MULTILINE" desc="A string split over several lines">
+ She gathered
+wood, charcoal, and
+a sledge hammer.
+ </message>
+ <message name="IDS_WHITESPACE" desc="A string with extra whitespace.">
+ ''' How old fashioned -- she thought. '''
+ </message>
+ <message name="IDS_PLACEHOLDERS" desc="A string with placeholders">
+ I'll buy a <ph name="WAVELENGTH">%d<ex>200</ex></ph> nm laser at <ph name="STORE_NAME">%s<ex>the grocery store</ex></ph>.
+ </message>
+ <message name="IDS_PLURALS" desc="A string using the ICU plural format">
+ {NUM_THINGS, plural,
+ =1 {Maybe I'll get one laser.}
+ other {Maybe I'll get # lasers.}}
+ </message>
+ <message name="IDS_PLURALS_NO_SPACE" desc="A string using the ICU plural format with no space">
+ {NUM_MISSISSIPPIS, plural,
+ =1{OneMississippi}other{ManyMississippis}}
+ </message>
+ </messages>
+ """)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('android', 'en'), buf)
+ output = buf.getvalue()
+ expected = r"""
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<string name="simple">"Martha"</string>
+<string name="one_line">"sat and wondered"</string>
+<string name="quotes">"out loud, \"Why don\'t I build a flying car?\""</string>
+<string name="multiline">"She gathered
+wood, charcoal, and
+a sledge hammer."</string>
+<string name="whitespace">" How old fashioned -- she thought. "</string>
+<string name="placeholders">"I\'ll buy a %d nm laser at %s."</string>
+<plurals name="plurals">
+ <item quantity="one">"Maybe I\'ll get one laser."</item>
+ <item quantity="other">"Maybe I\'ll get %d lasers."</item>
+</plurals>
+<plurals name="plurals_no_space">
+ <item quantity="one">"OneMississippi"</item>
+ <item quantity="other">"ManyMississippis"</item>
+</plurals>
+</resources>
+"""
+ self.assertEqual(output.strip(), expected.strip())
+
+
+ def testConflictingPlurals(self):
+ root = util.ParseGrdForUnittest(r"""
+ <messages>
+ <message name="IDS_PLURALS" desc="A string using the ICU plural format">
+ {NUM_THINGS, plural,
+ =1 {Maybe I'll get one laser.}
+ one {Maybe I'll get one laser.}
+ other {Maybe I'll get # lasers.}}
+ </message>
+ </messages>
+ """)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('android', 'en'), buf)
+ output = buf.getvalue()
+ expected = r"""
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<plurals name="plurals">
+ <item quantity="one">"Maybe I\'ll get one laser."</item>
+ <item quantity="other">"Maybe I\'ll get %d lasers."</item>
+</plurals>
+</resources>
+"""
+ self.assertEqual(output.strip(), expected.strip())
+
+
+ def testTaggedOnly(self):
+ root = util.ParseGrdForUnittest(r"""
+ <messages>
+ <message name="IDS_HELLO" desc="" formatter_data="android_java">
+ Hello
+ </message>
+ <message name="IDS_WORLD" desc="">
+ world
+ </message>
+ </messages>
+ """)
+
+ msg_hello, msg_world = root.GetChildrenOfType(message.MessageNode)
+ self.assertTrue(android_xml.ShouldOutputNode(msg_hello, tagged_only=True))
+ self.assertFalse(android_xml.ShouldOutputNode(msg_world, tagged_only=True))
+ self.assertTrue(android_xml.ShouldOutputNode(msg_hello, tagged_only=False))
+ self.assertTrue(android_xml.ShouldOutputNode(msg_world, tagged_only=False))
+
+
+class DummyOutput(object):
+
+ def __init__(self, type, language):
+ self.type = type
+ self.language = language
+
+ def GetType(self):
+ return self.type
+
+ def GetLanguage(self):
+ return self.language
+
+ def GetOutputFilename(self):
+ return 'hello.gif'
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/c_format.py b/tools/grit/grit/format/c_format.py
new file mode 100644
index 0000000000..16809a9f70
--- /dev/null
+++ b/tools/grit/grit/format/c_format.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Formats as a .C file for compilation.
+"""
+
+from __future__ import print_function
+
+import codecs
+import os
+import re
+
+import six
+
+from grit import util
+
+
+def _FormatHeader(root, output_dir):
+ """Returns the required preamble for C files."""
+ # Find the location of the resource header file, so that we can include
+ # it.
+ resource_header = 'resource.h' # fall back to this
+ for output in root.GetOutputFiles():
+ if output.attrs['type'] == 'rc_header':
+ resource_header = os.path.abspath(output.GetOutputFilename())
+ resource_header = util.MakeRelativePath(output_dir, resource_header)
+ return """// This file is automatically generated by GRIT. Do not edit.
+
+#include "%s"
+
+// All strings are UTF-8
+""" % (resource_header)
+# end _FormatHeader() function
+
+
+def Format(root, lang='en', output_dir='.'):
+ """Outputs a C switch statement representing the string table."""
+ from grit.node import message
+ assert isinstance(lang, six.string_types)
+
+ yield _FormatHeader(root, output_dir)
+
+ yield 'const char* GetString(int id) {\n switch (id) {'
+
+ for item in root.ActiveDescendants():
+ with item:
+ if isinstance(item, message.MessageNode):
+ yield _FormatMessage(item, lang)
+
+ yield '\n default:\n return 0;\n }\n}\n'
+
+
+def _HexToOct(match):
+ "Return the octal form of the hex numbers"
+ hex = match.group("hex")
+ result = ""
+ while len(hex):
+ next_num = int(hex[2:4], 16)
+ result += "\\" + '%03o' % next_num
+ hex = hex[4:]
+ return match.group("escaped_backslashes") + result
+
+
+def _FormatMessage(item, lang):
+ """Format a single <message> element."""
+
+ message = item.ws_at_start + item.Translate(lang) + item.ws_at_end
+ # Output message with non-ascii chars escaped as octal numbers C's grammar
+ # allows escaped hexadecimal numbers to be infinite, but octal is always of
+ # the form \OOO. Python 3 doesn't support string-escape, so we have to jump
+ # through some hoops here via codecs.escape_encode.
+ # This basically does:
+ # - message - the starting string
+ # - message.encode(...) - convert to bytes
+ # - codecs.escape_encode(...) - convert non-ASCII bytes to \x## escapes
+ # - (...).decode() - convert bytes back to a string
+ message = codecs.escape_encode(message.encode('utf-8'))[0].decode('utf-8')
+ # an escaped char is (\xHH)+ but only if the initial
+ # backslash is not escaped.
+ not_a_backslash = r"(^|[^\\])" # beginning of line or a non-backslash char
+ escaped_backslashes = not_a_backslash + r"(\\\\)*"
+ hex_digits = r"((\\x)[0-9a-f]{2})+"
+ two_digit_hex_num = re.compile(
+ r"(?P<escaped_backslashes>%s)(?P<hex>%s)"
+ % (escaped_backslashes, hex_digits))
+ message = two_digit_hex_num.sub(_HexToOct, message)
+ # unescape \ (convert \\ back to \)
+ message = message.replace('\\\\', '\\')
+ message = message.replace('"', '\\"')
+ message = util.LINEBREAKS.sub(r'\\n', message)
+
+ name_attr = item.GetTextualIds()[0]
+
+ return '\n case %s:\n return "%s";' % (name_attr, message)
diff --git a/tools/grit/grit/format/c_format_unittest.py b/tools/grit/grit/format/c_format_unittest.py
new file mode 100644
index 0000000000..380120c42f
--- /dev/null
+++ b/tools/grit/grit/format/c_format_unittest.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for c_format.py.
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import util
+from grit.tool import build
+
+
+class CFormatUnittest(unittest.TestCase):
+
+ def testMessages(self):
+ root = util.ParseGrdForUnittest(u"""
+ <messages>
+ <message name="IDS_QUESTIONS">Do you want to play questions?</message>
+ <message name="IDS_QUOTES">
+ "What's in a name, <ph name="NAME">%s<ex>Brandon</ex></ph>?"
+ </message>
+ <message name="IDS_LINE_BREAKS">
+ Was that rhetoric?
+No.
+Statement. Two all. Game point.
+</message>
+ <message name="IDS_NON_ASCII">
+ \u00f5\\xc2\\xa4\\\u00a4\\\\xc3\\xb5\u4924
+ </message>
+ </messages>
+ """)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('c_format', 'en'), buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+ self.assertEqual(u"""\
+#include "resource.h"
+const char* GetString(int id) {
+ switch (id) {
+ case IDS_QUESTIONS:
+ return "Do you want to play questions?";
+ case IDS_QUOTES:
+ return "\\"What\\'s in a name, %s?\\"";
+ case IDS_LINE_BREAKS:
+ return "Was that rhetoric?\\nNo.\\nStatement. Two all. Game point.";
+ case IDS_NON_ASCII:
+ return "\\303\\265\\xc2\\xa4\\\\302\\244\\\\xc3\\xb5\\344\\244\\244";
+ default:
+ return 0;
+ }
+}""", output)
+
+
+class DummyOutput(object):
+
+ def __init__(self, type, language):
+ self.type = type
+ self.language = language
+
+ def GetType(self):
+ return self.type
+
+ def GetLanguage(self):
+ return self.language
+
+ def GetOutputFilename(self):
+ return 'hello.gif'
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/chrome_messages_json.py b/tools/grit/grit/format/chrome_messages_json.py
new file mode 100644
index 0000000000..88ec1d914b
--- /dev/null
+++ b/tools/grit/grit/format/chrome_messages_json.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Formats as a .json file that can be used to localize Google Chrome
+extensions."""
+
+from __future__ import print_function
+
+from json import JSONEncoder
+
+from grit import constants
+from grit.node import message
+
+def Format(root, lang='en', output_dir='.'):
+ """Format the messages as JSON."""
+ yield '{'
+
+ encoder = JSONEncoder(ensure_ascii=False)
+ format = '"%s":{"message":%s%s}'
+ placeholder_format = '"%i":{"content":"$%i"}'
+ first = True
+ for child in root.ActiveDescendants():
+ if isinstance(child, message.MessageNode):
+ id = child.attrs['name']
+ if id.startswith('IDR_') or id.startswith('IDS_'):
+ id = id[4:]
+
+ translation_missing = child.GetCliques()[0].clique.get(lang) is None;
+ if (child.ShouldFallbackToEnglish() and translation_missing and
+ lang != constants.FAKE_BIDI):
+ # Skip the string if it's not translated. Chrome will fallback
+ # to English automatically.
+ continue
+
+ loc_message = encoder.encode(child.ws_at_start + child.Translate(lang) +
+ child.ws_at_end)
+
+ # Replace $n place-holders with $n$ and add an appropriate "placeholders"
+ # entry. Note that chrome.i18n.getMessage only supports 9 placeholders:
+ # https://developer.chrome.com/extensions/i18n#method-getMessage
+ placeholders = ''
+ for i in range(1, 10):
+ if loc_message.find('$%d' % i) == -1:
+ break
+ loc_message = loc_message.replace('$%d' % i, '$%d$' % i)
+ if placeholders:
+ placeholders += ','
+ placeholders += placeholder_format % (i, i)
+
+ if not first:
+ yield ','
+ first = False
+
+ if placeholders:
+ placeholders = ',"placeholders":{%s}' % placeholders
+ yield format % (id, loc_message, placeholders)
+
+ yield '}'
diff --git a/tools/grit/grit/format/chrome_messages_json_unittest.py b/tools/grit/grit/format/chrome_messages_json_unittest.py
new file mode 100644
index 0000000000..a54e6bdc1c
--- /dev/null
+++ b/tools/grit/grit/format/chrome_messages_json_unittest.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for chrome_messages_json.py.
+"""
+
+from __future__ import print_function
+
+import json
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit import util
+from grit.tool import build
+
+class ChromeMessagesJsonFormatUnittest(unittest.TestCase):
+
+ # The default unittest diff limit is too low for our unittests.
+ # Allow the framework to show the full diff output all the time.
+ maxDiff = None
+
+ def testMessages(self):
+ root = util.ParseGrdForUnittest(u"""
+ <messages>
+ <message name="IDS_SIMPLE_MESSAGE">
+ Simple message.
+ </message>
+ <message name="IDS_QUOTES">
+ element\u2019s \u201c<ph name="NAME">%s<ex>name</ex></ph>\u201d attribute
+ </message>
+ <message name="IDS_PLACEHOLDERS">
+ <ph name="ERROR_COUNT">%1$d<ex>1</ex></ph> error, <ph name="WARNING_COUNT">%2$d<ex>1</ex></ph> warning
+ </message>
+ <message name="IDS_PLACEHOLDERS_SUBSTITUTED_BY_GETMESSAGE">
+ <ph name="BEGIN">$1<ex>a</ex></ph>test<ph name="END">$2<ex>b</ex></ph>
+ </message>
+ <message name="IDS_STARTS_WITH_SPACE">
+ ''' (<ph name="COUNT">%d<ex>2</ex></ph>)
+ </message>
+ <message name="IDS_ENDS_WITH_SPACE">
+ (<ph name="COUNT">%d<ex>2</ex></ph>) '''
+ </message>
+ <message name="IDS_SPACE_AT_BOTH_ENDS">
+ ''' (<ph name="COUNT">%d<ex>2</ex></ph>) '''
+ </message>
+ <message name="IDS_DOUBLE_QUOTES">
+ A "double quoted" message.
+ </message>
+ <message name="IDS_BACKSLASH">
+ \\
+ </message>
+ </messages>
+ """)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'en'),
+ buf)
+ output = buf.getvalue()
+ test = u"""
+{
+ "SIMPLE_MESSAGE": {
+ "message": "Simple message."
+ },
+ "QUOTES": {
+ "message": "element\u2019s \u201c%s\u201d attribute"
+ },
+ "PLACEHOLDERS": {
+ "message": "%1$d error, %2$d warning"
+ },
+ "PLACEHOLDERS_SUBSTITUTED_BY_GETMESSAGE": {
+ "message": "$1$test$2$",
+ "placeholders": {
+ "1": {
+ "content": "$1"
+ },
+ "2": {
+ "content": "$2"
+ }
+ }
+ },
+ "STARTS_WITH_SPACE": {
+ "message": " (%d)"
+ },
+ "ENDS_WITH_SPACE": {
+ "message": "(%d) "
+ },
+ "SPACE_AT_BOTH_ENDS": {
+ "message": " (%d) "
+ },
+ "DOUBLE_QUOTES": {
+ "message": "A \\"double quoted\\" message."
+ },
+ "BACKSLASH": {
+ "message": "\\\\"
+ }
+}
+"""
+ self.assertEqual(json.loads(test), json.loads(output))
+
+ def testTranslations(self):
+ root = util.ParseGrdForUnittest("""
+ <messages>
+ <message name="ID_HELLO">Hello!</message>
+ <message name="ID_HELLO_USER">Hello <ph name="USERNAME">%s<ex>
+ Joi</ex></ph></message>
+ </messages>
+ """)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'fr'),
+ buf)
+ output = buf.getvalue()
+ test = u"""
+{
+ "ID_HELLO": {
+ "message": "H\u00e9P\u00e9ll\u00f4P\u00f4!"
+ },
+ "ID_HELLO_USER": {
+ "message": "H\u00e9P\u00e9ll\u00f4P\u00f4 %s"
+ }
+}
+"""
+ self.assertEqual(json.loads(test), json.loads(output))
+
+ def testSkipMissingTranslations(self):
+ grd = """<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" current_release="3" source_lang_id="en"
+ base_dir="%s">
+ <outputs>
+ </outputs>
+ <release seq="3" allow_pseudo="False">
+ <messages fallback_to_english="true">
+ <message name="ID_HELLO_NO_TRANSLATION">Hello not translated</message>
+ </messages>
+ </release>
+</grit>"""
+ root = grd_reader.Parse(StringIO(grd), dir=".")
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'fr'),
+ buf)
+ output = buf.getvalue()
+ test = u'{}'
+ self.assertEqual(test, output)
+
+ def testVerifyMinification(self):
+ root = util.ParseGrdForUnittest(u"""
+ <messages>
+ <message name="IDS">
+ <ph name="BEGIN">$1<ex>a</ex></ph>test<ph name="END">$2<ex>b</ex></ph>
+ </message>
+ </messages>
+ """)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'en'),
+ buf)
+ output = buf.getvalue()
+ test = (u'{"IDS":{"message":"$1$test$2$","placeholders":'
+ u'{"1":{"content":"$1"},"2":{"content":"$2"}}}}')
+ self.assertEqual(test, output)
+
+
+class DummyOutput(object):
+
+ def __init__(self, type, language):
+ self.type = type
+ self.language = language
+
+ def GetType(self):
+ return self.type
+
+ def GetLanguage(self):
+ return self.language
+
+ def GetOutputFilename(self):
+ return 'hello.gif'
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/data_pack.py b/tools/grit/grit/format/data_pack.py
new file mode 100644
index 0000000000..f7128a4725
--- /dev/null
+++ b/tools/grit/grit/format/data_pack.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Support for formatting a data pack file used for platform agnostic resource
+files.
+"""
+
+from __future__ import print_function
+
+import collections
+import os
+import struct
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import six
+
+from grit import util
+from grit.node import include
+from grit.node import message
+from grit.node import structure
+
+
+PACK_FILE_VERSION = 5
+BINARY, UTF8, UTF16 = range(3)
+
+
+GrdInfoItem = collections.namedtuple('GrdInfoItem',
+ ['textual_id', 'id', 'path'])
+
+
+class WrongFileVersion(Exception):
+ pass
+
+
+class CorruptDataPack(Exception):
+ pass
+
+
+class DataPackSizes(object):
+ def __init__(self, header, id_table, alias_table, data):
+ self.header = header
+ self.id_table = id_table
+ self.alias_table = alias_table
+ self.data = data
+
+ @property
+ def total(self):
+ return sum(v for v in self.__dict__.values())
+
+ def __iter__(self):
+ yield ('header', self.header)
+ yield ('id_table', self.id_table)
+ yield ('alias_table', self.alias_table)
+ yield ('data', self.data)
+
+ def __eq__(self, other):
+ return self.__dict__ == other.__dict__
+
+ def __repr__(self):
+ return self.__class__.__name__ + repr(self.__dict__)
+
+
+class DataPackContents(object):
+ def __init__(self, resources, encoding, version, aliases, sizes):
+ # Map of resource_id -> str.
+ self.resources = resources
+ # Encoding (int).
+ self.encoding = encoding
+ # Version (int).
+ self.version = version
+ # Map of resource_id->canonical_resource_id
+ self.aliases = aliases
+ # DataPackSizes instance.
+ self.sizes = sizes
+
+
+def Format(root, lang='en', output_dir='.'):
+ """Writes out the data pack file format (platform agnostic resource file)."""
+ id_map = root.GetIdMap()
+ data = {}
+ root.info = []
+ for node in root.ActiveDescendants():
+ with node:
+ if isinstance(node, (include.IncludeNode, message.MessageNode,
+ structure.StructureNode)):
+ value = node.GetDataPackValue(lang, util.BINARY)
+ if value is not None:
+ resource_id = id_map[node.GetTextualIds()[0]]
+ data[resource_id] = value
+ root.info.append('{},{},{}'.format(
+ node.attrs.get('name'), resource_id, node.source))
+ return WriteDataPackToString(data, UTF8)
+
+
+def ReadDataPack(input_file):
+ return ReadDataPackFromString(util.ReadFile(input_file, util.BINARY))
+
+
+def ReadDataPackFromString(data):
+ """Reads a data pack file and returns a dictionary."""
+ # Read the header.
+ version = struct.unpack('<I', data[:4])[0]
+ if version == 4:
+ resource_count, encoding = struct.unpack('<IB', data[4:9])
+ alias_count = 0
+ header_size = 9
+ elif version == 5:
+ encoding, resource_count, alias_count = struct.unpack('<BxxxHH', data[4:12])
+ header_size = 12
+ else:
+ raise WrongFileVersion('Found version: ' + str(version))
+
+ resources = {}
+ kIndexEntrySize = 2 + 4 # Each entry is a uint16 and a uint32.
+ def entry_at_index(idx):
+ offset = header_size + idx * kIndexEntrySize
+ return struct.unpack('<HI', data[offset:offset + kIndexEntrySize])
+
+ prev_resource_id, prev_offset = entry_at_index(0)
+ for i in range(1, resource_count + 1):
+ resource_id, offset = entry_at_index(i)
+ resources[prev_resource_id] = data[prev_offset:offset]
+ prev_resource_id, prev_offset = resource_id, offset
+
+ id_table_size = (resource_count + 1) * kIndexEntrySize
+ # Read the alias table.
+ kAliasEntrySize = 2 + 2 # uint16, uint16
+ def alias_at_index(idx):
+ offset = header_size + id_table_size + idx * kAliasEntrySize
+ return struct.unpack('<HH', data[offset:offset + kAliasEntrySize])
+
+ aliases = {}
+ for i in range(alias_count):
+ resource_id, index = alias_at_index(i)
+ aliased_id = entry_at_index(index)[0]
+ aliases[resource_id] = aliased_id
+ resources[resource_id] = resources[aliased_id]
+
+ alias_table_size = kAliasEntrySize * alias_count
+ sizes = DataPackSizes(
+ header_size, id_table_size, alias_table_size,
+ len(data) - header_size - id_table_size - alias_table_size)
+ assert sizes.total == len(data), 'original={} computed={}'.format(
+ len(data), sizes.total)
+ return DataPackContents(resources, encoding, version, aliases, sizes)
+
+
+def WriteDataPackToString(resources, encoding):
+ """Returns bytes with a map of id=>data in the data pack format."""
+ ret = []
+
+ # Compute alias map.
+ resource_ids = sorted(resources)
+ # Use reversed() so that for duplicates lower IDs clobber higher ones.
+ id_by_data = {resources[k]: k for k in reversed(resource_ids)}
+ # Map of resource_id -> resource_id, where value < key.
+ alias_map = {k: id_by_data[v] for k, v in resources.items()
+ if id_by_data[v] != k}
+
+ # Write file header.
+ resource_count = len(resources) - len(alias_map)
+ # Padding bytes added for alignment.
+ ret.append(struct.pack('<IBxxxHH', PACK_FILE_VERSION, encoding,
+ resource_count, len(alias_map)))
+ HEADER_LENGTH = 4 + 4 + 2 + 2
+
+ # Each main table entry is: uint16 + uint32 (and an extra entry at the end).
+ # Each alias table entry is: uint16 + uint16.
+ data_offset = HEADER_LENGTH + (resource_count + 1) * 6 + len(alias_map) * 4
+
+ # Write main table.
+ index_by_id = {}
+ deduped_data = []
+ index = 0
+ for resource_id in resource_ids:
+ if resource_id in alias_map:
+ continue
+ data = resources[resource_id]
+ if isinstance(data, six.text_type):
+ data = data.encode('utf-8')
+ index_by_id[resource_id] = index
+ ret.append(struct.pack('<HI', resource_id, data_offset))
+ data_offset += len(data)
+ deduped_data.append(data)
+ index += 1
+
+ assert index == resource_count
+ # Add an extra entry at the end.
+ ret.append(struct.pack('<HI', 0, data_offset))
+
+ # Write alias table.
+ for resource_id in sorted(alias_map):
+ index = index_by_id[alias_map[resource_id]]
+ ret.append(struct.pack('<HH', resource_id, index))
+
+ # Write data.
+ ret.extend(deduped_data)
+ return b''.join(ret)
+
+
+def WriteDataPack(resources, output_file, encoding):
+ """Writes a map of id=>data into output_file as a data pack."""
+ content = WriteDataPackToString(resources, encoding)
+ with open(output_file, 'wb') as file:
+ file.write(content)
+
+
+def ReadGrdInfo(grd_file):
+ info_dict = {}
+ with open(grd_file + '.info', 'rt') as f:
+ for line in f:
+ item = GrdInfoItem._make(line.strip().split(','))
+ info_dict[int(item.id)] = item
+ return info_dict
+
+
+def RePack(output_file, input_files, whitelist_file=None,
+ suppress_removed_key_output=False,
+ output_info_filepath=None):
+ """Write a new data pack file by combining input pack files.
+
+ Args:
+ output_file: path to the new data pack file.
+ input_files: a list of paths to the data pack files to combine.
+ whitelist_file: path to the file that contains the list of resource IDs
+ that should be kept in the output file or None to include
+ all resources.
+ suppress_removed_key_output: allows the caller to suppress the output from
+ RePackFromDataPackStrings.
+ output_info_file: If not None, specify the output .info filepath.
+
+ Raises:
+ KeyError: if there are duplicate keys or resource encoding is
+ inconsistent.
+ """
+ input_data_packs = [ReadDataPack(filename) for filename in input_files]
+ input_info_files = [filename + '.info' for filename in input_files]
+ whitelist = None
+ if whitelist_file:
+ lines = util.ReadFile(whitelist_file, 'utf-8').strip().splitlines()
+ if not lines:
+ raise Exception('Whitelist file should not be empty')
+ whitelist = set(int(x) for x in lines)
+ inputs = [(p.resources, p.encoding) for p in input_data_packs]
+ resources, encoding = RePackFromDataPackStrings(
+ inputs, whitelist, suppress_removed_key_output)
+ WriteDataPack(resources, output_file, encoding)
+ if output_info_filepath is None:
+ output_info_filepath = output_file + '.info'
+ with open(output_info_filepath, 'w') as output_info_file:
+ for filename in input_info_files:
+ with open(filename, 'r') as info_file:
+ output_info_file.writelines(info_file.readlines())
+
+
+def RePackFromDataPackStrings(inputs, whitelist,
+ suppress_removed_key_output=False):
+ """Combines all inputs into one.
+
+ Args:
+ inputs: a list of (resources_by_id, encoding) tuples to be combined.
+ whitelist: a list of resource IDs that should be kept in the output string
+ or None to include all resources.
+ suppress_removed_key_output: Do not print removed keys.
+
+ Returns:
+ Returns (resources_by_id, encoding).
+
+ Raises:
+ KeyError: if there are duplicate keys or resource encoding is
+ inconsistent.
+ """
+ resources = {}
+ encoding = None
+ for input_resources, input_encoding in inputs:
+ # Make sure we have no dups.
+ duplicate_keys = set(input_resources.keys()) & set(resources.keys())
+ if duplicate_keys:
+ raise KeyError('Duplicate keys: ' + str(list(duplicate_keys)))
+
+ # Make sure encoding is consistent.
+ if encoding in (None, BINARY):
+ encoding = input_encoding
+ elif input_encoding not in (BINARY, encoding):
+ raise KeyError('Inconsistent encodings: ' + str(encoding) +
+ ' vs ' + str(input_encoding))
+
+ if whitelist:
+ whitelisted_resources = dict([(key, input_resources[key])
+ for key in input_resources.keys()
+ if key in whitelist])
+ resources.update(whitelisted_resources)
+ removed_keys = [key for key in input_resources.keys()
+ if key not in whitelist]
+ if not suppress_removed_key_output:
+ for key in removed_keys:
+ print('RePackFromDataPackStrings Removed Key:', key)
+ else:
+ resources.update(input_resources)
+
+ # Encoding is 0 for BINARY, 1 for UTF8 and 2 for UTF16
+ if encoding is None:
+ encoding = BINARY
+ return resources, encoding
+
+
+def main():
+ # Write a simple file.
+ data = {1: '', 4: 'this is id 4', 6: 'this is id 6', 10: ''}
+ WriteDataPack(data, 'datapack1.pak', UTF8)
+ data2 = {1000: 'test', 5: 'five'}
+ WriteDataPack(data2, 'datapack2.pak', UTF8)
+ print('wrote datapack1 and datapack2 to current directory.')
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/grit/grit/format/data_pack_unittest.py b/tools/grit/grit/format/data_pack_unittest.py
new file mode 100644
index 0000000000..fcd7035473
--- /dev/null
+++ b/tools/grit/grit/format/data_pack_unittest.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.data_pack'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit.format import data_pack
+
+
+class FormatDataPackUnittest(unittest.TestCase):
+ def testReadDataPackV4(self):
+ expected_data = (
+ b'\x04\x00\x00\x00' # header(version
+ b'\x04\x00\x00\x00' # no. entries,
+ b'\x01' # encoding)
+ b'\x01\x00\x27\x00\x00\x00' # index entry 1
+ b'\x04\x00\x27\x00\x00\x00' # index entry 4
+ b'\x06\x00\x33\x00\x00\x00' # index entry 6
+ b'\x0a\x00\x3f\x00\x00\x00' # index entry 10
+ b'\x00\x00\x3f\x00\x00\x00' # extra entry for the size of last
+ b'this is id 4this is id 6') # data
+ expected_data_pack = data_pack.DataPackContents(
+ {
+ 1: b'',
+ 4: b'this is id 4',
+ 6: b'this is id 6',
+ 10: b'',
+ }, data_pack.UTF8, 4, {}, data_pack.DataPackSizes(9, 30, 0, 24))
+ loaded = data_pack.ReadDataPackFromString(expected_data)
+ self.assertDictEqual(expected_data_pack.__dict__, loaded.__dict__)
+
+ def testReadWriteDataPackV5(self):
+ expected_data = (
+ b'\x05\x00\x00\x00' # version
+ b'\x01\x00\x00\x00' # encoding & padding
+ b'\x03\x00' # resource_count
+ b'\x01\x00' # alias_count
+ b'\x01\x00\x28\x00\x00\x00' # index entry 1
+ b'\x04\x00\x28\x00\x00\x00' # index entry 4
+ b'\x06\x00\x34\x00\x00\x00' # index entry 6
+ b'\x00\x00\x40\x00\x00\x00' # extra entry for the size of last
+ b'\x0a\x00\x01\x00' # alias table
+ b'this is id 4this is id 6') # data
+ input_resources = {
+ 1: b'',
+ 4: b'this is id 4',
+ 6: b'this is id 6',
+ 10: b'this is id 4',
+ }
+ data = data_pack.WriteDataPackToString(input_resources, data_pack.UTF8)
+ self.assertEquals(data, expected_data)
+
+ expected_data_pack = data_pack.DataPackContents({
+ 1: b'',
+ 4: input_resources[4],
+ 6: input_resources[6],
+ 10: input_resources[4],
+ }, data_pack.UTF8, 5, {10: 4}, data_pack.DataPackSizes(12, 24, 4, 24))
+ loaded = data_pack.ReadDataPackFromString(expected_data)
+ self.assertDictEqual(expected_data_pack.__dict__, loaded.__dict__)
+
+ def testRePackUnittest(self):
+ expected_with_whitelist = {
+ 1: 'Never gonna', 10: 'give you up', 20: 'Never gonna let',
+ 30: 'you down', 40: 'Never', 50: 'gonna run around and',
+ 60: 'desert you'}
+ expected_without_whitelist = {
+ 1: 'Never gonna', 10: 'give you up', 20: 'Never gonna let', 65: 'Close',
+ 30: 'you down', 40: 'Never', 50: 'gonna run around and', 4: 'click',
+ 60: 'desert you', 6: 'chirr', 32: 'oops, try again', 70: 'Awww, snap!'}
+ inputs = [{1: 'Never gonna', 4: 'click', 6: 'chirr', 10: 'give you up'},
+ {20: 'Never gonna let', 30: 'you down', 32: 'oops, try again'},
+ {40: 'Never', 50: 'gonna run around and', 60: 'desert you'},
+ {65: 'Close', 70: 'Awww, snap!'}]
+ whitelist = [1, 10, 20, 30, 40, 50, 60]
+ inputs = [(i, data_pack.UTF8) for i in inputs]
+
+ # RePack using whitelist
+ output, _ = data_pack.RePackFromDataPackStrings(
+ inputs, whitelist, suppress_removed_key_output=True)
+ self.assertDictEqual(expected_with_whitelist, output,
+ 'Incorrect resource output')
+
+ # RePack a None whitelist
+ output, _ = data_pack.RePackFromDataPackStrings(
+ inputs, None, suppress_removed_key_output=True)
+ self.assertDictEqual(expected_without_whitelist, output,
+ 'Incorrect resource output')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/gen_predetermined_ids.py b/tools/grit/grit/format/gen_predetermined_ids.py
new file mode 100644
index 0000000000..9b2aa7b1a5
--- /dev/null
+++ b/tools/grit/grit/format/gen_predetermined_ids.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+A tool to generate a predetermined resource ids file that can be used as an
+input to grit via the -p option. This is meant to be run manually every once in
+a while and its output checked in. See tools/gritsettings/README.md for details.
+"""
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+
+# Regular expression for parsing the #define macro format. Matches both the
+# version of the macro with whitelist support and the one without. For example,
+# Without generate whitelist flag:
+# #define IDS_FOO_MESSAGE 1234
+# With generate whitelist flag:
+# #define IDS_FOO_MESSAGE (::ui::WhitelistedResource<1234>(), 1234)
+RESOURCE_EXTRACT_REGEX = re.compile(r'^#define (\S*).* (\d+)\)?$', re.MULTILINE)
+
+ORDERED_RESOURCE_IDS_REGEX = re.compile(r'^Resource=(\d*)$', re.MULTILINE)
+
+
+def _GetResourceNameIdPairsIter(string_to_scan):
+ """Gets an iterator of the resource name and id pairs of the given string.
+
+ Scans the input string for lines of the form "#define NAME ID" and returns
+ an iterator over all matching (NAME, ID) pairs.
+
+ Args:
+ string_to_scan: The input string to scan.
+
+ Yields:
+ A tuple of name and id.
+ """
+ for match in RESOURCE_EXTRACT_REGEX.finditer(string_to_scan):
+ yield match.group(1, 2)
+
+
+def _ReadOrderedResourceIds(path):
+ """Reads ordered resource ids from the given file.
+
+ The resources are expected to be of the format produced by running Chrome
+ with --print-resource-ids command line.
+
+ Args:
+ path: File path to read resource ids from.
+
+ Returns:
+ An array of ordered resource ids.
+ """
+ ordered_resource_ids = []
+ with open(path, "r") as f:
+ for match in ORDERED_RESOURCE_IDS_REGEX.finditer(f.read()):
+ ordered_resource_ids.append(int(match.group(1)))
+ return ordered_resource_ids
+
+
+def GenerateResourceMapping(original_resources, ordered_resource_ids):
+ """Generates a resource mapping from the ordered ids and the original mapping.
+
+ The returned dict will assign new ids to ordered_resource_ids numerically
+ increasing from 101.
+
+ Args:
+ original_resources: A dict of original resource ids to resource names.
+ ordered_resource_ids: An array of ordered resource ids.
+
+ Returns:
+ A dict of resource ids to resource names.
+ """
+ output_resource_map = {}
+ # 101 is used as the starting value since other parts of GRIT require it to be
+ # the minimum (e.g. rc_header.py) based on Windows resource numbering.
+ next_id = 101
+ for original_id in ordered_resource_ids:
+ resource_name = original_resources[original_id]
+ output_resource_map[next_id] = resource_name
+ next_id += 1
+ return output_resource_map
+
+
+def ReadResourceIdsFromFile(file, original_resources):
+ """Reads resource ids from a GRIT-produced header file.
+
+ Args:
+ file: File to a GRIT-produced header file to read from.
+ original_resources: Dict of resource ids to resource names to add to.
+ """
+ for resource_name, resource_id in _GetResourceNameIdPairsIter(file.read()):
+ original_resources[int(resource_id)] = resource_name
+
+
+def _ReadOriginalResourceIds(out_dir):
+ """Reads resource ids from GRIT header files in the specified directory.
+
+ Args:
+ out_dir: A Chrome build output directory (e.g. out/gn) to scan.
+
+ Returns:
+ A dict of resource ids to resource names.
+ """
+ original_resources = {}
+ for root, dirnames, filenames in os.walk(out_dir + '/gen'):
+ for filename in filenames:
+ if filename.endswith(('_resources.h', '_settings.h', '_strings.h')):
+ with open(os.path.join(root, filename), "r") as f:
+ ReadResourceIdsFromFile(f, original_resources)
+ return original_resources
+
+
+def _GeneratePredeterminedIdsFile(ordered_resources_file, out_dir):
+ """Generates a predetermined ids file.
+
+ Args:
+ ordered_resources_file: File path to read ordered resource ids from.
+ out_dir: A Chrome build output directory (e.g. out/gn) to scan.
+
+ Returns:
+ A dict of resource ids to resource names.
+ """
+ original_resources = _ReadOriginalResourceIds(out_dir)
+ ordered_resource_ids = _ReadOrderedResourceIds(ordered_resources_file)
+ output_resource_map = GenerateResourceMapping(original_resources,
+ ordered_resource_ids)
+ for res_id in sorted(output_resource_map.keys()):
+ print(output_resource_map[res_id], res_id)
+
+
+def main(argv):
+ if len(argv) != 2:
+ print("usage: gen_predetermined_ids.py <ordered_resources_file> <out_dir>")
+ sys.exit(1)
+ ordered_resources_file, out_dir = argv[0], argv[1]
+ _GeneratePredeterminedIdsFile(ordered_resources_file, out_dir)
+
+
+if '__main__' == __name__:
+ main(sys.argv[1:])
diff --git a/tools/grit/grit/format/gen_predetermined_ids_unittest.py b/tools/grit/grit/format/gen_predetermined_ids_unittest.py
new file mode 100644
index 0000000000..bd0331adb4
--- /dev/null
+++ b/tools/grit/grit/format/gen_predetermined_ids_unittest.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for the gen_predetermined_ids module.'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.format import gen_predetermined_ids
+
+class GenPredeterminedIdsUnittest(unittest.TestCase):
+ def testGenerateResourceMapping(self):
+ original_resources = {200: 'A', 201: 'B', 300: 'C', 350: 'D', 370: 'E'}
+ ordered_resource_ids = [300, 201, 370]
+ mapping = gen_predetermined_ids.GenerateResourceMapping(
+ original_resources, ordered_resource_ids)
+ self.assertEqual({101: 'C', 102: 'B', 103: 'E'}, mapping)
+
+ def testReadResourceIdsFromFile(self):
+ f = StringIO('''
+// This file is automatically generated by GRIT. Do not edit.
+
+#pragma once
+
+#define IDS_BOOKMARKS_NO_ITEMS 12500
+#define IDS_BOOKMARK_BAR_IMPORT_LINK (::ui::WhitelistedResource<12501>(), 12501)
+#define IDS_BOOKMARK_X (::ui::WhitelistedResource<12502>(), 12502)
+''')
+ resources = {}
+ gen_predetermined_ids.ReadResourceIdsFromFile(f, resources)
+ self.assertEqual({12500: 'IDS_BOOKMARKS_OPEN_ALL',
+ 12501: 'IDS_BOOKMARKS_OPEN_ALL_INCOGNITO',
+ 12502: 'IDS_BOOKMARK_X'}, resources)
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/gzip_string.py b/tools/grit/grit/format/gzip_string.py
new file mode 100644
index 0000000000..3cd17185c9
--- /dev/null
+++ b/tools/grit/grit/format/gzip_string.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Provides gzip utilities for strings.
+"""
+
+from __future__ import print_function
+
+import gzip
+import io
+import subprocess
+
+
+def GzipStringRsyncable(data):
+ # Make call to host system's gzip to get access to --rsyncable option. This
+ # option makes updates much smaller - if one line is changed in the resource,
+ # it won't have to push the entire compressed resource with the update.
+ # Instead, --rsyncable breaks the file into small chunks, so that one doesn't
+ # affect the other in compression, and then only that chunk will have to be
+ # updated.
+ gzip_proc = subprocess.Popen(['gzip', '--stdout', '--rsyncable',
+ '--best', '--no-name'],
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ data, stderr = gzip_proc.communicate(data)
+ if gzip_proc.returncode != 0:
+ raise subprocess.CalledProcessError(gzip_proc.returncode, 'gzip',
+ stderr)
+ return data
+
+
+def GzipString(data):
+ # Gzipping using Python's built in gzip: Windows doesn't ship with gzip, and
+ # OSX's gzip does not have an --rsyncable option built in. Although this is
+ # not preferable to --rsyncable, it is an option for the systems that do
+ # not have --rsyncable. If used over GzipStringRsyncable, the primary
+ # difference of this function's compression will be larger updates every time
+ # a compressed resource is changed.
+ gzip_output = io.BytesIO()
+ with gzip.GzipFile(mode='wb', compresslevel=9, fileobj=gzip_output,
+ mtime=0) as gzip_file:
+ gzip_file.write(data)
+ data = gzip_output.getvalue()
+ gzip_output.close()
+ return data
diff --git a/tools/grit/grit/format/gzip_string_unittest.py b/tools/grit/grit/format/gzip_string_unittest.py
new file mode 100644
index 0000000000..c0cfbe1837
--- /dev/null
+++ b/tools/grit/grit/format/gzip_string_unittest.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# Copyright (c) 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.gzip_string'''
+
+from __future__ import print_function
+
+import gzip
+import io
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit.format import gzip_string
+
+
+class FormatGzipStringUnittest(unittest.TestCase):
+
+ def testGzipStringRsyncable(self):
+ # Can only test the rsyncable version on platforms which support rsyncable,
+ # which at the moment is Linux.
+ if sys.platform == 'linux2':
+ header_begin = (b'\x1f\x8b') # gzip first two bytes
+ input = (b'TEST STRING STARTING NOW'
+ b'continuing'
+ b'<even more>'
+ b'<finished NOW>')
+
+ compressed = gzip_string.GzipStringRsyncable(input)
+ self.failUnless(header_begin == compressed[:2])
+
+ compressed_file = io.BytesIO()
+ compressed_file.write(compressed)
+ compressed_file.seek(0)
+
+ with gzip.GzipFile(mode='rb', fileobj=compressed_file) as f:
+ output = f.read()
+ self.failUnless(output == input)
+
+ def testGzipString(self):
+ header_begin = b'\x1f\x8b' # gzip first two bytes
+ input = (b'TEST STRING STARTING NOW'
+ b'continuing'
+ b'<even more>'
+ b'<finished NOW>')
+
+ compressed = gzip_string.GzipString(input)
+ self.failUnless(header_begin == compressed[:2])
+
+ compressed_file = io.BytesIO()
+ compressed_file.write(compressed)
+ compressed_file.seek(0)
+
+ with gzip.GzipFile(mode='rb', fileobj=compressed_file) as f:
+ output = f.read()
+ self.failUnless(output == input)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/html_inline.py b/tools/grit/grit/format/html_inline.py
new file mode 100644
index 0000000000..da55216ea4
--- /dev/null
+++ b/tools/grit/grit/format/html_inline.py
@@ -0,0 +1,602 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Flattens a HTML file by inlining its external resources.
+
+This is a small script that takes a HTML file, looks for src attributes
+and inlines the specified file, producing one HTML file with no external
+dependencies. It recursively inlines the included files.
+"""
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+import base64
+import mimetypes
+
+from grit import lazy_re
+from grit import util
+from grit.format import minifier
+
+# There is a python bug that makes mimetypes crash if the Windows
+# registry contains non-Latin keys ( http://bugs.python.org/issue9291
+# ). Initing manually and blocking external mime-type databases will
+# prevent that bug and if we add svg manually, it will still give us
+# the data we need.
+mimetypes.init([])
+mimetypes.add_type('image/svg+xml', '.svg')
+
+# webm video type is not always available if mimetype package is outdated.
+mimetypes.add_type('video/webm', '.webm')
+
+DIST_DEFAULT = 'chromium'
+DIST_ENV_VAR = 'CHROMIUM_BUILD'
+DIST_SUBSTR = '%DISTRIBUTION%'
+
+# Matches beginning of an "if" block.
+_BEGIN_IF_BLOCK = lazy_re.compile(
+ r'<if [^>]*?expr=("(?P<expr1>[^">]*)"|\'(?P<expr2>[^\'>]*)\')[^>]*?>')
+
+# Matches ending of an "if" block.
+_END_IF_BLOCK = lazy_re.compile(r'</if>')
+
+# Used by DoInline to replace various links with inline content.
+_STYLESHEET_RE = lazy_re.compile(
+ r'<link rel="stylesheet"[^>]+?href="(?P<filename>[^"]*)".*?>(\s*</link>)?',
+ re.DOTALL)
+_INCLUDE_RE = lazy_re.compile(
+ r'(?P<comment>\/\/ )?<include[^>]+?'
+ r'src=("(?P<file1>[^">]*)"|\'(?P<file2>[^\'>]*)\').*?>(\s*</include>)?',
+ re.DOTALL)
+_SRC_RE = lazy_re.compile(
+ r'<(?!script)(?:[^>]+?\s)src="(?!\[\[|{{)(?P<filename>[^"\']*)"',
+ re.MULTILINE)
+# This re matches '<img srcset="..."' or '<source srcset="..."'
+_SRCSET_RE = lazy_re.compile(
+ r'<(img|source)\b(?:[^>]*?\s)srcset="(?!\[\[|{{|\$i18n{)'
+ r'(?P<srcset>[^"\']*)"',
+ re.MULTILINE)
+# This re is for splitting srcset value string into "image candidate strings".
+# Notes:
+# - HTML 5.2 states that URL cannot start or end with comma.
+# - the "descriptor" is either "width descriptor" or "pixel density descriptor".
+# The first one consists of "valid non-negative integer + letter 'x'",
+# the second one is formed of "positive valid floating-point number +
+# letter 'w'". As a reasonable compromise, we match a list of characters
+# that form both of them.
+# Matches for example "img2.png 2x" or "img9.png 11E-2w".
+_SRCSET_ENTRY_RE = lazy_re.compile(
+ r'\s*(?P<url>[^,\s]\S+[^,\s])'
+ r'(?:\s+(?P<descriptor>[\deE.-]+[wx]))?\s*'
+ r'(?P<separator>,|$)',
+ re.MULTILINE)
+_ICON_RE = lazy_re.compile(
+ r'<link rel="icon"\s(?:[^>]+?\s)?'
+ r'href=(?P<quote>")(?P<filename>[^"\']*)\1',
+ re.MULTILINE)
+
+
+def GetDistribution():
+ """Helper function that gets the distribution we are building.
+
+ Returns:
+ string
+ """
+ distribution = DIST_DEFAULT
+ if DIST_ENV_VAR in os.environ:
+ distribution = os.environ[DIST_ENV_VAR]
+ if len(distribution) > 1 and distribution[0] == '_':
+ distribution = distribution[1:].lower()
+ return distribution
+
+def ConvertFileToDataURL(filename, base_path, distribution, inlined_files,
+ names_only):
+ """Convert filename to inlined data URI.
+
+ Takes a filename from ether "src" or "srcset", and attempts to read the file
+ at 'filename'. Returns data URI as string with given file inlined.
+ If it finds DIST_SUBSTR string in file name, replaces it with distribution.
+ If filename contains ':', it is considered URL and not translated.
+
+ Args:
+ filename: filename string from ether src or srcset attributes.
+ base_path: path that to look for files in
+ distribution: string that should replace DIST_SUBSTR
+ inlined_files: The name of the opened file is appended to this list.
+ names_only: If true, the function will not read the file but just return "".
+ It will still add the filename to |inlined_files|.
+
+ Returns:
+ string
+ """
+ if filename.find(':') != -1:
+ # filename is probably a URL, which we don't want to bother inlining
+ return filename
+
+ filename = filename.replace(DIST_SUBSTR , distribution)
+ filepath = os.path.normpath(os.path.join(base_path, filename))
+ inlined_files.add(filepath)
+
+ if names_only:
+ return ""
+
+ mimetype = mimetypes.guess_type(filename)[0]
+ if mimetype is None:
+ raise Exception('%s is of an an unknown type and '
+ 'cannot be stored in a data url.' % filename)
+ inline_data = base64.standard_b64encode(util.ReadFile(filepath, util.BINARY))
+ return 'data:%s;base64,%s' % (mimetype, inline_data.decode('utf-8'))
+
+
+def SrcInlineAsDataURL(
+ src_match, base_path, distribution, inlined_files, names_only=False,
+ filename_expansion_function=None):
+ """regex replace function.
+
+ Takes a regex match for src="filename", attempts to read the file
+ at 'filename' and returns the src attribute with the file inlined
+ as a data URI. If it finds DIST_SUBSTR string in file name, replaces
+ it with distribution.
+
+ Args:
+ src_match: regex match object with 'filename' named capturing group
+ base_path: path that to look for files in
+ distribution: string that should replace DIST_SUBSTR
+ inlined_files: The name of the opened file is appended to this list.
+ names_only: If true, the function will not read the file but just return "".
+ It will still add the filename to |inlined_files|.
+
+ Returns:
+ string
+ """
+ filename = src_match.group('filename')
+ if filename_expansion_function:
+ filename = filename_expansion_function(filename)
+
+ data_url = ConvertFileToDataURL(filename, base_path, distribution,
+ inlined_files, names_only)
+
+ if not data_url:
+ return data_url
+
+ prefix = src_match.string[src_match.start():src_match.start('filename')]
+ suffix = src_match.string[src_match.end('filename'):src_match.end()]
+ return prefix + data_url + suffix
+
+def SrcsetInlineAsDataURL(
+ srcset_match, base_path, distribution, inlined_files, names_only=False,
+ filename_expansion_function=None):
+ """regex replace function to inline files in srcset="..." attributes
+
+ Takes a regex match for srcset="filename 1x, filename 2x, ...", attempts to
+ read the files referenced by filenames and returns the srcset attribute with
+ the files inlined as a data URI. If it finds DIST_SUBSTR string in file name,
+ replaces it with distribution.
+
+ Args:
+ srcset_match: regex match object with 'srcset' named capturing group
+ base_path: path that to look for files in
+ distribution: string that should replace DIST_SUBSTR
+ inlined_files: The name of the opened file is appended to this list.
+ names_only: If true, the function will not read the file but just return "".
+ It will still add the filename to |inlined_files|.
+
+ Returns:
+ string
+ """
+ srcset = srcset_match.group('srcset')
+
+ if not srcset:
+ return srcset_match.group(0)
+
+ # HTML 5.2 defines srcset as a list of "image candidate strings".
+ # Each of them consists of URL and descriptor.
+ # _SRCSET_ENTRY_RE splits srcset into a list of URLs, descriptors and
+ # commas.
+ # The descriptor part will be None if that optional regex didn't match
+ parts = _SRCSET_ENTRY_RE.split(srcset)
+
+ if not parts:
+ return srcset_match.group(0)
+
+ # List of image candidate strings that will form new srcset="..."
+ new_candidates = []
+
+ # When iterating over split srcset we fill this parts of a single image
+ # candidate string: [url, descriptor]
+ candidate = [];
+
+ # Each entry should consist of some text before the entry, the url,
+ # the descriptor or None if the entry has no descriptor, a comma separator or
+ # the end of the line, and finally some text after the entry (which is the
+ # same as the text before the next entry).
+ for i in range(0, len(parts) - 1, 4):
+ before, url, descriptor, separator, after = parts[i:i+5]
+
+ # There must be a comma-separated next entry or this must be the last entry.
+ assert separator == "," or (separator == "" and i == len(parts) - 5), (
+ "Bad srcset format in {}".format(srcset_match.group(0)))
+ # Both before and after the entry must be empty
+ assert before == after == "", (
+ "Bad srcset format in {}".format(srcset_match.group(0)))
+
+ if filename_expansion_function:
+ filename = filename_expansion_function(url)
+ else:
+ filename = url
+
+ data_url = ConvertFileToDataURL(filename, base_path, distribution,
+ inlined_files, names_only)
+
+ # This is not "names_only" mode
+ if data_url:
+ candidate = [data_url]
+ if descriptor:
+ candidate.append(descriptor)
+
+ new_candidates.append(" ".join(candidate))
+
+ prefix = srcset_match.string[srcset_match.start():
+ srcset_match.start('srcset')]
+ suffix = srcset_match.string[srcset_match.end('srcset'):srcset_match.end()]
+ return prefix + ','.join(new_candidates) + suffix
+
+class InlinedData:
+ """Helper class holding the results from DoInline().
+
+ Holds the inlined data and the set of filenames of all the inlined
+ files.
+ """
+ def __init__(self, inlined_data, inlined_files):
+ self.inlined_data = inlined_data
+ self.inlined_files = inlined_files
+
+def DoInline(
+ input_filename, grd_node, allow_external_script=False,
+ preprocess_only=False, names_only=False, strip_whitespace=False,
+ rewrite_function=None, filename_expansion_function=None):
+ """Helper function that inlines the resources in a specified file.
+
+ Reads input_filename, finds all the src attributes and attempts to
+ inline the files they are referring to, then returns the result and
+ the set of inlined files.
+
+ Args:
+ input_filename: name of file to read in
+ grd_node: html node from the grd file for this include tag
+ preprocess_only: Skip all HTML processing, only handle <if> and <include>.
+ names_only: |nil| will be returned for the inlined contents (faster).
+ strip_whitespace: remove whitespace and comments in the input files.
+ rewrite_function: function(filepath, text, distribution) which will be
+ called to rewrite html content before inlining images.
+ filename_expansion_function: function(filename) which will be called to
+ rewrite filenames before attempting to read them.
+ Returns:
+ a tuple of the inlined data as a string and the set of filenames
+ of all the inlined files
+ """
+ if filename_expansion_function:
+ input_filename = filename_expansion_function(input_filename)
+ input_filepath = os.path.dirname(input_filename)
+ distribution = GetDistribution()
+
+ # Keep track of all the files we inline.
+ inlined_files = set()
+
+ def SrcReplace(src_match, filepath=input_filepath,
+ inlined_files=inlined_files):
+ """Helper function to provide SrcInlineAsDataURL with the base file path"""
+ return SrcInlineAsDataURL(
+ src_match, filepath, distribution, inlined_files, names_only=names_only,
+ filename_expansion_function=filename_expansion_function)
+
+ def SrcsetReplace(srcset_match, filepath=input_filepath,
+ inlined_files=inlined_files):
+ """Helper function to provide SrcsetInlineAsDataURL with the base file
+ path.
+ """
+ return SrcsetInlineAsDataURL(
+ srcset_match, filepath, distribution, inlined_files,
+ names_only=names_only,
+ filename_expansion_function=filename_expansion_function)
+
+ def GetFilepath(src_match, base_path = input_filepath):
+ filename = [v for k, v in src_match.groupdict().items()
+ if k.startswith('file') and v][0]
+
+ if filename.find(':') != -1:
+ # filename is probably a URL, which we don't want to bother inlining
+ return None
+
+ filename = filename.replace('%DISTRIBUTION%', distribution)
+ if filename_expansion_function:
+ filename = filename_expansion_function(filename)
+ return os.path.normpath(os.path.join(base_path, filename))
+
+ def IsConditionSatisfied(src_match):
+ expr1 = src_match.group('expr1') or ''
+ expr2 = src_match.group('expr2') or ''
+ return grd_node is None or grd_node.EvaluateCondition(expr1 + expr2)
+
+ def CheckConditionalElements(str):
+ """Helper function to conditionally inline inner elements"""
+ while True:
+ begin_if = _BEGIN_IF_BLOCK.search(str)
+ if begin_if is None:
+ if _END_IF_BLOCK.search(str) is not None:
+ raise Exception('Unmatched </if>')
+ return str
+
+ condition_satisfied = IsConditionSatisfied(begin_if)
+ leading = str[0:begin_if.start()]
+ content_start = begin_if.end()
+
+ # Find matching "if" block end.
+ count = 1
+ pos = begin_if.end()
+ while True:
+ end_if = _END_IF_BLOCK.search(str, pos)
+ if end_if is None:
+ raise Exception('Unmatched <if>')
+
+ next_if = _BEGIN_IF_BLOCK.search(str, pos)
+ if next_if is None or next_if.start() >= end_if.end():
+ count = count - 1
+ if count == 0:
+ break
+ pos = end_if.end()
+ else:
+ count = count + 1
+ pos = next_if.end()
+
+ content = str[content_start:end_if.start()]
+ trailing = str[end_if.end():]
+
+ if condition_satisfied:
+ str = leading + CheckConditionalElements(content) + trailing
+ else:
+ str = leading + trailing
+
+ def InlineFileContents(src_match,
+ pattern,
+ inlined_files=inlined_files,
+ strip_whitespace=False):
+ """Helper function to inline external files of various types"""
+ filepath = GetFilepath(src_match)
+ if filepath is None:
+ return src_match.group(0)
+ inlined_files.add(filepath)
+
+ if names_only:
+ inlined_files.update(GetResourceFilenames(
+ filepath,
+ grd_node,
+ allow_external_script,
+ rewrite_function,
+ filename_expansion_function=filename_expansion_function))
+ return ""
+ # To recursively save inlined files, we need InlinedData instance returned
+ # by DoInline.
+ inlined_data_inst=DoInline(filepath, grd_node,
+ allow_external_script=allow_external_script,
+ preprocess_only=preprocess_only,
+ strip_whitespace=strip_whitespace,
+ filename_expansion_function=filename_expansion_function)
+
+ inlined_files.update(inlined_data_inst.inlined_files)
+
+ return pattern % inlined_data_inst.inlined_data;
+
+
+ def InlineIncludeFiles(src_match):
+ """Helper function to directly inline generic external files (without
+ wrapping them with any kind of tags).
+ """
+ return InlineFileContents(src_match, '%s')
+
+ def InlineScript(match):
+ """Helper function to inline external script files"""
+ attrs = (match.group('attrs1') + match.group('attrs2')).strip()
+ if attrs:
+ attrs = ' ' + attrs
+ return InlineFileContents(match, '<script' + attrs + '>%s</script>',
+ strip_whitespace=True)
+
+ def InlineCSSText(text, css_filepath):
+ """Helper function that inlines external resources in CSS text"""
+ filepath = os.path.dirname(css_filepath)
+ # Allow custom modifications before inlining images.
+ if rewrite_function:
+ text = rewrite_function(filepath, text, distribution)
+ text = InlineCSSImages(text, filepath)
+ return InlineCSSImports(text, filepath)
+
+ def InlineCSSFile(src_match, pattern, base_path=input_filepath):
+ """Helper function to inline external CSS files.
+
+ Args:
+ src_match: A regular expression match with a named group named "filename".
+ pattern: The pattern to replace with the contents of the CSS file.
+ base_path: The base path to use for resolving the CSS file.
+
+ Returns:
+ The text that should replace the reference to the CSS file.
+ """
+ filepath = GetFilepath(src_match, base_path)
+ if filepath is None:
+ return src_match.group(0)
+
+ # Even if names_only is set, the CSS file needs to be opened, because it
+ # can link to images that need to be added to the file set.
+ inlined_files.add(filepath)
+
+ # Inline stylesheets included in this css file.
+ text = _INCLUDE_RE.sub(InlineIncludeFiles, util.ReadFile(filepath, 'utf-8'))
+ # When resolving CSS files we need to pass in the path so that relative URLs
+ # can be resolved.
+
+ return pattern % InlineCSSText(text, filepath)
+
+ def GetUrlRegexString(postfix=''):
+ """Helper function that returns a string for a regex that matches url('')
+ but not url([[ ]]) or url({{ }}). Appends |postfix| to group names.
+ """
+ url_re = (r'url\((?!\[\[|{{)(?P<q%s>"|\'|)(?P<filename%s>[^"\'()]*)'
+ r'(?P=q%s)\)')
+ return url_re % (postfix, postfix, postfix)
+
+ def InlineCSSImages(text, filepath=input_filepath):
+ """Helper function that inlines external images in CSS backgrounds."""
+ # Replace contents of url() for css attributes: content, background,
+ # or *-image.
+ property_re = r'(content|background|[\w-]*-image):[^;]*'
+ # Replace group names to prevent duplicates when forming value_re.
+ image_set_value_re = (r'image-set\(([ ]*' + GetUrlRegexString('2') +
+ r'[ ]*[0-9.]*x[ ]*(,[ ]*)?)+\)')
+ value_re = '(%s|%s)' % (GetUrlRegexString(), image_set_value_re)
+ css_re = property_re + value_re
+ return re.sub(css_re, lambda m: InlineCSSUrls(m, filepath), text)
+
+ def InlineCSSUrls(src_match, filepath=input_filepath):
+ """Helper function that inlines each url on a CSS image rule match."""
+ # Replace contents of url() references in matches.
+ return re.sub(GetUrlRegexString(),
+ lambda m: SrcReplace(m, filepath),
+ src_match.group(0))
+
+ def InlineCSSImports(text, filepath=input_filepath):
+ """Helper function that inlines CSS files included via the @import
+ directive.
+ """
+ return re.sub(r'@import\s+' + GetUrlRegexString() + r';',
+ lambda m: InlineCSSFile(m, '%s', filepath),
+ text)
+
+
+ flat_text = util.ReadFile(input_filename, 'utf-8')
+
+ # Check conditional elements, remove unsatisfied ones from the file. We do
+ # this twice. The first pass is so that we don't even bother calling
+ # InlineScript, InlineCSSFile and InlineIncludeFiles on text we're eventually
+ # going to throw out anyway.
+ flat_text = CheckConditionalElements(flat_text)
+
+ flat_text = _INCLUDE_RE.sub(InlineIncludeFiles, flat_text)
+
+ if not preprocess_only:
+ if strip_whitespace:
+ flat_text = minifier.Minify(flat_text.encode('utf-8'),
+ input_filename).decode('utf-8')
+
+ if not allow_external_script:
+ # We need to inline css and js before we inline images so that image
+ # references gets inlined in the css and js
+ flat_text = re.sub(r'<script (?P<attrs1>.*?)src="(?P<filename>[^"\']*)"'
+ r'(?P<attrs2>.*?)></script>',
+ InlineScript,
+ flat_text)
+
+ flat_text = _STYLESHEET_RE.sub(
+ lambda m: InlineCSSFile(m, '<style>%s</style>'),
+ flat_text)
+
+ # Check conditional elements, second pass. This catches conditionals in any
+ # of the text we just inlined.
+ flat_text = CheckConditionalElements(flat_text)
+
+ # Allow custom modifications before inlining images.
+ if rewrite_function:
+ flat_text = rewrite_function(input_filepath, flat_text, distribution)
+
+ if not preprocess_only:
+ flat_text = _SRC_RE.sub(SrcReplace, flat_text)
+ flat_text = _SRCSET_RE.sub(SrcsetReplace, flat_text)
+
+ # TODO(arv): Only do this inside <style> tags.
+ flat_text = InlineCSSImages(flat_text)
+
+ flat_text = _ICON_RE.sub(SrcReplace, flat_text)
+
+ if names_only:
+ flat_text = None # Will contains garbage if the flag is set anyway.
+ return InlinedData(flat_text, inlined_files)
+
+
+def InlineToString(input_filename, grd_node, preprocess_only = False,
+ allow_external_script=False, strip_whitespace=False,
+ rewrite_function=None, filename_expansion_function=None):
+ """Inlines the resources in a specified file and returns it as a string.
+
+ Args:
+ input_filename: name of file to read in
+ grd_node: html node from the grd file for this include tag
+ Returns:
+ the inlined data as a string
+ """
+ try:
+ return DoInline(
+ input_filename,
+ grd_node,
+ preprocess_only=preprocess_only,
+ allow_external_script=allow_external_script,
+ strip_whitespace=strip_whitespace,
+ rewrite_function=rewrite_function,
+ filename_expansion_function=filename_expansion_function).inlined_data
+ except IOError as e:
+ raise Exception("Failed to open %s while trying to flatten %s. (%s)" %
+ (e.filename, input_filename, e.strerror))
+
+
+def InlineToFile(input_filename, output_filename, grd_node):
+ """Inlines the resources in a specified file and writes it.
+
+ Reads input_filename, finds all the src attributes and attempts to
+ inline the files they are referring to, then writes the result
+ to output_filename.
+
+ Args:
+ input_filename: name of file to read in
+ output_filename: name of file to be written to
+ grd_node: html node from the grd file for this include tag
+ Returns:
+ a set of filenames of all the inlined files
+ """
+ inlined_data = InlineToString(input_filename, grd_node)
+ with open(output_filename, 'wb') as out_file:
+ out_file.write(inlined_data)
+
+
+def GetResourceFilenames(filename,
+ grd_node,
+ allow_external_script=False,
+ rewrite_function=None,
+ filename_expansion_function=None):
+ """For a grd file, returns a set of all the files that would be inline."""
+ try:
+ return DoInline(
+ filename,
+ grd_node,
+ names_only=True,
+ preprocess_only=False,
+ allow_external_script=allow_external_script,
+ strip_whitespace=False,
+ rewrite_function=rewrite_function,
+ filename_expansion_function=filename_expansion_function).inlined_files
+ except IOError as e:
+ raise Exception("Failed to open %s while trying to flatten %s. (%s)" %
+ (e.filename, filename, e.strerror))
+
+
+def main():
+ if len(sys.argv) <= 2:
+ print("Flattens a HTML file by inlining its external resources.\n")
+ print("html_inline.py inputfile outputfile")
+ else:
+ InlineToFile(sys.argv[1], sys.argv[2], None)
+
+if __name__ == '__main__':
+ main()
diff --git a/tools/grit/grit/format/html_inline_unittest.py b/tools/grit/grit/format/html_inline_unittest.py
new file mode 100644
index 0000000000..1b11e9e476
--- /dev/null
+++ b/tools/grit/grit/format/html_inline_unittest.py
@@ -0,0 +1,927 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.html_inline'''
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit import util
+from grit.format import html_inline
+
+
+class HtmlInlineUnittest(unittest.TestCase):
+ '''Unit tests for HtmlInline.'''
+
+ def testGetResourceFilenames(self):
+ '''Tests that all included files are returned by GetResourceFilenames.'''
+
+ files = {
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ <link rel="stylesheet"
+ href="really-long-long-long-long-long-test.css">
+ </head>
+ <body>
+ <include src='test.html'>
+ <include
+ src="really-long-long-long-long-long-test-file-omg-so-long.html">
+ </body>
+ </html>
+ ''',
+
+ 'test.html': '''
+ <include src="test2.html">
+ ''',
+
+ 'really-long-long-long-long-long-test-file-omg-so-long.html': '''
+ <!-- This really long named resource should be included. -->
+ ''',
+
+ 'test2.html': '''
+ <!-- This second level resource should also be included. -->
+ ''',
+
+ 'test.css': '''
+ .image {
+ background: url('test.png');
+ }
+ ''',
+
+ 'really-long-long-long-long-long-test.css': '''
+ a:hover {
+ font-weight: bold; /* Awesome effect is awesome! */
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+ }
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ resources = html_inline.GetResourceFilenames(tmp_dir.GetPath('index.html'),
+ None)
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ tmp_dir.CleanUp()
+
+ def testUnmatchedEndIfBlock(self):
+ '''Tests that an unmatched </if> raises an exception.'''
+
+ files = {
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <if expr="lang == 'fr'">
+ bonjour
+ </if>
+ <if expr='lang == "de"'>
+ hallo
+ </if>
+ </if>
+ </html>
+ ''',
+ }
+
+ tmp_dir = util.TempDir(files)
+
+ with self.assertRaises(Exception) as cm:
+ html_inline.GetResourceFilenames(tmp_dir.GetPath('index.html'), None)
+ self.failUnlessEqual(str(cm.exception), 'Unmatched </if>')
+ tmp_dir.CleanUp()
+
+ def testCompressedJavaScript(self):
+ '''Tests that ".src=" doesn't treat as a tag.'''
+
+ files = {
+ 'index.js': '''
+ if(i<j)a.src="hoge.png";
+ ''',
+ }
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ resources = html_inline.GetResourceFilenames(tmp_dir.GetPath('index.js'),
+ None)
+ resources.add(tmp_dir.GetPath('index.js'))
+ self.failUnlessEqual(resources, source_resources)
+ tmp_dir.CleanUp()
+
+ def testInlineCSSImports(self):
+ '''Tests that @import directives in inlined CSS files are inlined too.
+ '''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="css/test.css">
+ </head>
+ </html>
+ ''',
+
+ 'css/test.css': '''
+ @import url('test2.css');
+ blink {
+ display: none;
+ }
+ ''',
+
+ 'css/test2.css': '''
+ .image {
+ background: url('../images/test.png');
+ }
+ '''.strip(),
+
+ 'images/test.png': 'PNG DATA'
+ }
+
+ expected_inlined = '''
+ <html>
+ <head>
+ <style>
+ .image {
+ background: url('data:image/png;base64,UE5HIERBVEE=');
+ }
+ blink {
+ display: none;
+ }
+ </style>
+ </head>
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(util.normpath(filename)))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+
+ tmp_dir.CleanUp()
+
+ def testInlineIgnoresPolymerBindings(self):
+ '''Tests that polymer bindings are ignored when inlining.
+ '''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ </head>
+ <body>
+ <iron-icon src="[[icon]]"></iron-icon><!-- Should be ignored. -->
+ <iron-icon src="{{src}}"></iron-icon><!-- Also ignored. -->
+ <!-- [[image]] should be ignored. -->
+ <div style="background: url([[image]]),
+ url('test.png');">
+ </div>
+ <div style="background: url('test.png'),
+ url([[image]]);">
+ </div>
+ </body>
+ </html>
+ ''',
+
+ 'test.css': '''
+ .image {
+ background: url('test.png');
+ background-image: url([[ignoreMe]]);
+ background-image: image-set(url({{alsoMe}}), 1x);
+ background-image: image-set(
+ url({{ignore}}) 1x,
+ url('test.png') 2x);
+ }
+ ''',
+
+ 'test.png': 'PNG DATA'
+ }
+
+ expected_inlined = '''
+ <html>
+ <head>
+ <style>
+ .image {
+ background: url('data:image/png;base64,UE5HIERBVEE=');
+ background-image: url([[ignoreMe]]);
+ background-image: image-set(url({{alsoMe}}), 1x);
+ background-image: image-set(
+ url({{ignore}}) 1x,
+ url('data:image/png;base64,UE5HIERBVEE=') 2x);
+ }
+ </style>
+ </head>
+ <body>
+ <iron-icon src="[[icon]]"></iron-icon><!-- Should be ignored. -->
+ <iron-icon src="{{src}}"></iron-icon><!-- Also ignored. -->
+ <!-- [[image]] should be ignored. -->
+ <div style="background: url([[image]]),
+ url('data:image/png;base64,UE5HIERBVEE=');">
+ </div>
+ <div style="background: url('data:image/png;base64,UE5HIERBVEE='),
+ url([[image]]);">
+ </div>
+ </body>
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(util.normpath(filename)))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+
+ tmp_dir.CleanUp()
+
+ def testInlineCSSWithIncludeDirective(self):
+ '''Tests that include directive in external css files also inlined'''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="foo.css">
+ </head>
+ </html>
+ ''',
+
+ 'foo.css': '''<include src="style.css">''',
+
+ 'style.css': '''
+ <include src="style2.css">
+ blink {
+ display: none;
+ }
+ ''',
+ 'style2.css': '''h1 {}''',
+ }
+
+ expected_inlined = '''
+ <html>
+ <head>
+ <style>
+ h1 {}
+ blink {
+ display: none;
+ }
+ </style>
+ </head>
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testCssIncludedFileNames(self):
+ '''Tests that all included files from css are returned'''
+
+ files = {
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ </head>
+ <body>
+ </body>
+ </html>
+ ''',
+
+ 'test.css': '''
+ <include src="test2.css">
+ ''',
+
+ 'test2.css': '''
+ <include src="test3.css">
+ .image {
+ background: url('test.png');
+ }
+ ''',
+
+ 'test3.css': '''h1 {}''',
+
+ 'test.png': 'PNG DATA'
+ }
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ resources = html_inline.GetResourceFilenames(tmp_dir.GetPath('index.html'),
+ None)
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ tmp_dir.CleanUp()
+
+ def testInlineCSSLinks(self):
+ '''Tests that only CSS files referenced via relative URLs are inlined.'''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="foo.css">
+ <link rel="stylesheet" href="chrome://resources/bar.css">
+ </head>
+ </html>
+ ''',
+
+ 'foo.css': '''
+ @import url(chrome://resources/blurp.css);
+ blink {
+ display: none;
+ }
+ ''',
+ }
+
+ expected_inlined = '''
+ <html>
+ <head>
+ <style>
+ @import url(chrome://resources/blurp.css);
+ blink {
+ display: none;
+ }
+ </style>
+ <link rel="stylesheet" href="chrome://resources/bar.css">
+ </head>
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testFilenameVariableExpansion(self):
+ '''Tests that variables are expanded in filenames before inlining.'''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="style[WHICH].css">
+ <script src="script[WHICH].js"></script>
+ </head>
+ <include src="tmpl[WHICH].html">
+ <img src="img[WHICH].png">
+ </html>
+ ''',
+ 'style1.css': '''h1 {}''',
+ 'tmpl1.html': '''<h1></h1>''',
+ 'script1.js': '''console.log('hello');''',
+ 'img1.png': '''abc''',
+ }
+
+ expected_inlined = '''
+ <html>
+ <head>
+ <style>h1 {}</style>
+ <script>console.log('hello');</script>
+ </head>
+ <h1></h1>
+ <img src="data:image/png;base64,YWJj">
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ def replacer(var, repl):
+ return lambda filename: filename.replace('[%s]' % var, repl)
+
+ # Test normal inlining.
+ result = html_inline.DoInline(
+ tmp_dir.GetPath('index.html'),
+ None,
+ filename_expansion_function=replacer('WHICH', '1'))
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+
+ # Test names-only inlining.
+ result = html_inline.DoInline(
+ tmp_dir.GetPath('index.html'),
+ None,
+ names_only=True,
+ filename_expansion_function=replacer('WHICH', '1'))
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ tmp_dir.CleanUp()
+
+ def testWithCloseTags(self):
+ '''Tests that close tags are removed.'''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="style1.css"></link>
+ <link rel="stylesheet" href="style2.css">
+ </link>
+ <link rel="stylesheet" href="style2.css"
+ >
+ </link>
+ <script src="script1.js"></script>
+ </head>
+ <include src="tmpl1.html"></include>
+ <include src="tmpl2.html">
+ </include>
+ <include src="tmpl2.html"
+ >
+ </include>
+ <img src="img1.png">
+ <include src='single-double-quotes.html"></include>
+ <include src="double-single-quotes.html'></include>
+ </html>
+ ''',
+ 'style1.css': '''h1 {}''',
+ 'style2.css': '''h2 {}''',
+ 'tmpl1.html': '''<h1></h1>''',
+ 'tmpl2.html': '''<h2></h2>''',
+ 'script1.js': '''console.log('hello');''',
+ 'img1.png': '''abc''',
+ }
+
+ expected_inlined = '''
+ <html>
+ <head>
+ <style>h1 {}</style>
+ <style>h2 {}</style>
+ <style>h2 {}</style>
+ <script>console.log('hello');</script>
+ </head>
+ <h1></h1>
+ <h2></h2>
+ <h2></h2>
+ <img src="data:image/png;base64,YWJj">
+ <include src='single-double-quotes.html"></include>
+ <include src="double-single-quotes.html'></include>
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ # Test normal inlining.
+ result = html_inline.DoInline(
+ tmp_dir.GetPath('index.html'),
+ None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testCommentedJsInclude(self):
+ '''Tests that <include> works inside a comment.'''
+
+ files = {
+ 'include.js': '// <include src="other.js">',
+ 'other.js': '// Copyright somebody\nalert(1);',
+ }
+
+ expected_inlined = '// Copyright somebody\nalert(1);'
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('include.js'), None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('include.js'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testCommentedJsIf(self):
+ '''Tests that <if> works inside a comment.'''
+
+ files = {
+ 'if.js': '''
+ // <if expr="True">
+ yep();
+ // </if>
+
+ // <if expr="False">
+ nope();
+ // </if>
+ ''',
+ }
+
+ expected_inlined = '''
+ //
+ yep();
+ //
+
+ //
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ class FakeGrdNode(object):
+ def EvaluateCondition(self, cond):
+ return eval(cond)
+
+ result = html_inline.DoInline(tmp_dir.GetPath('if.js'), FakeGrdNode())
+ resources = result.inlined_files
+
+ resources.add(tmp_dir.GetPath('if.js'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testImgSrcset(self):
+ '''Tests that img srcset="" attributes are converted.'''
+
+ # Note that there is no space before "img10.png" and that
+ # "img11.png" has no descriptor.
+ files = {
+ 'index.html': '''
+ <html>
+ <img src="img1.png" srcset="img2.png 1x, img3.png 2x">
+ <img src="img4.png" srcset=" img5.png 1x , img6.png 2x ">
+ <img src="chrome://theme/img11.png" srcset="img7.png 1x, '''\
+ '''chrome://theme/img13.png 2x">
+ <img srcset="img8.png 300w, img9.png 11E-2w,img10.png -1e2w">
+ <img srcset="img11.png">
+ <img srcset="img11.png, img2.png 1x">
+ <img srcset="img2.png 1x, img11.png">
+ </html>
+ ''',
+ 'img1.png': '''a1''',
+ 'img2.png': '''a2''',
+ 'img3.png': '''a3''',
+ 'img4.png': '''a4''',
+ 'img5.png': '''a5''',
+ 'img6.png': '''a6''',
+ 'img7.png': '''a7''',
+ 'img8.png': '''a8''',
+ 'img9.png': '''a9''',
+ 'img10.png': '''a10''',
+ 'img11.png': '''a11''',
+ }
+
+ expected_inlined = '''
+ <html>
+ <img src="data:image/png;base64,YTE=" srcset="data:image/png;base64,'''\
+ '''YTI= 1x,data:image/png;base64,YTM= 2x">
+ <img src="data:image/png;base64,YTQ=" srcset="data:image/png;base64,'''\
+ '''YTU= 1x,data:image/png;base64,YTY= 2x">
+ <img src="chrome://theme/img11.png" srcset="data:image/png;base64,'''\
+ '''YTc= 1x,chrome://theme/img13.png 2x">
+ <img srcset="data:image/png;base64,YTg= 300w,data:image/png;base64,'''\
+ '''YTk= 11E-2w,data:image/png;base64,YTEw -1e2w">
+ <img srcset="data:image/png;base64,YTEx">
+ <img srcset="data:image/png;base64,YTEx,data:image/png;base64,YTI= 1x">
+ <img srcset="data:image/png;base64,YTI= 1x,data:image/png;base64,YTEx">
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ # Test normal inlining.
+ result = html_inline.DoInline(
+ tmp_dir.GetPath('index.html'),
+ None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testImgSrcsetIgnoresI18n(self):
+ '''Tests that $i18n{...} strings are ignored when inlining.
+ '''
+
+ src_html = '''
+ <html>
+ <head></head>
+ <body>
+ <img srcset="$i18n{foo}">
+ </body>
+ </html>
+ '''
+
+ files = {
+ 'index.html': src_html,
+ }
+
+ expected_inlined = src_html
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(util.normpath(filename)))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testSourceSrcset(self):
+ '''Tests that source srcset="" attributes are converted.'''
+
+ # Note that there is no space before "img10.png" and that
+ # "img11.png" has no descriptor.
+ files = {
+ 'index.html': '''
+ <html>
+ <source src="img1.png" srcset="img2.png 1x, img3.png 2x">
+ <source src="img4.png" srcset=" img5.png 1x , img6.png 2x ">
+ <source src="chrome://theme/img11.png" srcset="img7.png 1x, '''\
+ '''chrome://theme/img13.png 2x">
+ <source srcset="img8.png 300w, img9.png 11E-2w,img10.png -1e2w">
+ <source srcset="img11.png">
+ </html>
+ ''',
+ 'img1.png': '''a1''',
+ 'img2.png': '''a2''',
+ 'img3.png': '''a3''',
+ 'img4.png': '''a4''',
+ 'img5.png': '''a5''',
+ 'img6.png': '''a6''',
+ 'img7.png': '''a7''',
+ 'img8.png': '''a8''',
+ 'img9.png': '''a9''',
+ 'img10.png': '''a10''',
+ 'img11.png': '''a11''',
+ }
+
+ expected_inlined = '''
+ <html>
+ <source src="data:image/png;base64,YTE=" srcset="data:image/png;'''\
+ '''base64,YTI= 1x,data:image/png;base64,YTM= 2x">
+ <source src="data:image/png;base64,YTQ=" srcset="data:image/png;'''\
+ '''base64,YTU= 1x,data:image/png;base64,YTY= 2x">
+ <source src="chrome://theme/img11.png" srcset="data:image/png;'''\
+ '''base64,YTc= 1x,chrome://theme/img13.png 2x">
+ <source srcset="data:image/png;base64,YTg= 300w,data:image/png;'''\
+ '''base64,YTk= 11E-2w,data:image/png;base64,YTEw -1e2w">
+ <source srcset="data:image/png;base64,YTEx">
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ # Test normal inlining.
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+ self.failUnlessEqual(expected_inlined,
+ util.FixLineEnd(result.inlined_data, '\n'))
+ tmp_dir.CleanUp()
+
+ def testConditionalInclude(self):
+ '''Tests that output and dependency generation includes only files not'''\
+ ''' blocked by <if> macros.'''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <if expr="True">
+ <img src="img1.png" srcset="img2.png 1x, img3.png 2x">
+ </if>
+ <if expr="False">
+ <img src="img4.png" srcset=" img5.png 1x, img6.png 2x ">
+ </if>
+ <if expr="True">
+ <img src="chrome://theme/img11.png" srcset="img7.png 1x, '''\
+ '''chrome://theme/img13.png 2x">
+ </if>
+ <img srcset="img8.png 300w, img9.png 11E-2w,img10.png -1e2w">
+ </html>
+ ''',
+ 'img1.png': '''a1''',
+ 'img2.png': '''a2''',
+ 'img3.png': '''a3''',
+ 'img4.png': '''a4''',
+ 'img5.png': '''a5''',
+ 'img6.png': '''a6''',
+ 'img7.png': '''a7''',
+ 'img8.png': '''a8''',
+ 'img9.png': '''a9''',
+ 'img10.png': '''a10''',
+ }
+
+ expected_inlined = '''
+ <html>
+ <img src="data:image/png;base64,YTE=" srcset="data:image/png;base64,'''\
+ '''YTI= 1x,data:image/png;base64,YTM= 2x">
+ <img src="chrome://theme/img11.png" srcset="data:image/png;base64,'''\
+ '''YTc= 1x,chrome://theme/img13.png 2x">
+ <img srcset="data:image/png;base64,YTg= 300w,data:image/png;base64,'''\
+ '''YTk= 11E-2w,data:image/png;base64,YTEw -1e2w">
+ </html>
+ '''
+
+ expected_files = [
+ 'index.html',
+ 'img1.png',
+ 'img2.png',
+ 'img3.png',
+ 'img7.png',
+ 'img8.png',
+ 'img9.png',
+ 'img10.png'
+ ]
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ for filename in expected_files:
+ source_resources.add(tmp_dir.GetPath(filename))
+
+ class FakeGrdNode(object):
+ def EvaluateCondition(self, cond):
+ return eval(cond)
+
+ # Test normal inlining.
+ result = html_inline.DoInline(
+ tmp_dir.GetPath('index.html'),
+ FakeGrdNode())
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+
+ # ignore whitespace
+ expected_inlined = re.sub(r'\s+', ' ', expected_inlined)
+ actually_inlined = re.sub(r'\s+', ' ',
+ util.FixLineEnd(result.inlined_data, '\n'))
+ self.failUnlessEqual(expected_inlined, actually_inlined);
+ tmp_dir.CleanUp()
+
+ def testPreprocessOnlyEvaluatesIncludeAndIf(self):
+ '''Tests that preprocess_only=true evaluates <include> and <if> only. '''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="not_inlined.css">
+ <script src="also_not_inlined.js">
+ </head>
+ <body>
+ <include src="inline_this.html">
+ <if expr="True">
+ <p>'if' should be evaluated.</p>
+ </if>
+ </body>
+ </html>
+ ''',
+ 'not_inlined.css': ''' /* <link> should not be inlined. */ ''',
+ 'also_not_inlined.js': ''' // <script> should not be inlined. ''',
+ 'inline_this.html': ''' <p>'include' should be inlined.</p> '''
+ }
+
+ expected_inlined = '''
+ <html>
+ <head>
+ <link rel="stylesheet" href="not_inlined.css">
+ <script src="also_not_inlined.js">
+ </head>
+ <body>
+ <p>'include' should be inlined.</p>
+ <p>'if' should be evaluated.</p>
+ </body>
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ source_resources.add(tmp_dir.GetPath('index.html'))
+ source_resources.add(tmp_dir.GetPath('inline_this.html'))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None,
+ preprocess_only=True)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+
+ # Ignore whitespace
+ expected_inlined = re.sub(r'\s+', ' ', expected_inlined)
+ actually_inlined = re.sub(r'\s+', ' ',
+ util.FixLineEnd(result.inlined_data, '\n'))
+ self.failUnlessEqual(expected_inlined, actually_inlined)
+
+ tmp_dir.CleanUp()
+
+ def testPreprocessOnlyAppliesRecursively(self):
+ '''Tests that preprocess_only=true propagates to included files. '''
+
+ files = {
+ 'index.html': '''
+ <html>
+ <include src="outer_include.html">
+ </html>
+ ''',
+ 'outer_include.html': '''
+ <include src="inner_include.html">
+ <link rel="stylesheet" href="not_inlined.css">
+ ''',
+ 'inner_include.html': ''' <p>This should be inlined in index.html</p> ''',
+ 'not_inlined.css': ''' /* This should not be inlined. */ '''
+ }
+
+ expected_inlined = '''
+ <html>
+ <p>This should be inlined in index.html</p>
+ <link rel="stylesheet" href="not_inlined.css">
+ </html>
+ '''
+
+ source_resources = set()
+ tmp_dir = util.TempDir(files)
+ source_resources.add(tmp_dir.GetPath('index.html'))
+ source_resources.add(tmp_dir.GetPath('outer_include.html'))
+ source_resources.add(tmp_dir.GetPath('inner_include.html'))
+
+ result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None,
+ preprocess_only=True)
+ resources = result.inlined_files
+ resources.add(tmp_dir.GetPath('index.html'))
+ self.failUnlessEqual(resources, source_resources)
+
+ # Ignore whitespace
+ expected_inlined = re.sub(r'\s+', ' ', expected_inlined)
+ actually_inlined = re.sub(r'\s+', ' ',
+ util.FixLineEnd(result.inlined_data, '\n'))
+ self.failUnlessEqual(expected_inlined, actually_inlined)
+
+ tmp_dir.CleanUp()
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/minifier.py b/tools/grit/grit/format/minifier.py
new file mode 100644
index 0000000000..1a0ea34e49
--- /dev/null
+++ b/tools/grit/grit/format/minifier.py
@@ -0,0 +1,45 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Framework for stripping whitespace and comments from resource files"""
+
+from __future__ import print_function
+
+from os import path
+import subprocess
+import sys
+
+import six
+
+__js_minifier = None
+__css_minifier = None
+
+def SetJsMinifier(minifier):
+ global __js_minifier
+ __js_minifier = minifier.split()
+
+def SetCssMinifier(minifier):
+ global __css_minifier
+ __css_minifier = minifier.split()
+
+def Minify(source, filename):
+ """Minify |source| (bytes) from |filename| and return bytes."""
+ file_type = path.splitext(filename)[1]
+ minifier = None
+ if file_type == '.js':
+ minifier = __js_minifier
+ elif file_type == '.css':
+ minifier = __css_minifier
+ if not minifier:
+ return source
+ p = subprocess.Popen(
+ minifier,
+ stdin=subprocess.PIPE,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ (stdout, stderr) = p.communicate(source)
+ if p.returncode != 0:
+ print('Minification failed for %s' % filename)
+ print(stderr)
+ sys.exit(p.returncode)
+ return stdout
diff --git a/tools/grit/grit/format/policy_templates_json.py b/tools/grit/grit/format/policy_templates_json.py
new file mode 100644
index 0000000000..2f9330bb9a
--- /dev/null
+++ b/tools/grit/grit/format/policy_templates_json.py
@@ -0,0 +1,26 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Translates policy_templates.json files.
+"""
+
+from __future__ import print_function
+
+from grit.node import structure
+
+
+def Format(root, lang='en', output_dir='.'):
+ policy_json = None
+ for item in root.ActiveDescendants():
+ with item:
+ if (isinstance(item, structure.StructureNode) and
+ item.attrs['type'] == 'policy_template_metafile'):
+ json_text = item.gatherer.Translate(
+ lang,
+ pseudo_if_not_available=item.PseudoIsAllowed(),
+ fallback_to_english=item.ShouldFallbackToEnglish())
+ # We're only expecting one node of this kind.
+ assert not policy_json
+ policy_json = json_text
+ return policy_json
diff --git a/tools/grit/grit/format/policy_templates_json_unittest.py b/tools/grit/grit/format/policy_templates_json_unittest.py
new file mode 100644
index 0000000000..e252c94e2c
--- /dev/null
+++ b/tools/grit/grit/format/policy_templates_json_unittest.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+# coding: utf-8
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for policy_templates_json.py.
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import grit.extern.tclib
+import tempfile
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit.tool import build
+
+
+class PolicyTemplatesJsonUnittest(unittest.TestCase):
+
+ def testPolicyTranslation(self):
+ # Create test policy_templates.json data.
+ caption = "The main policy"
+ caption_translation = "Die Hauptrichtlinie"
+
+ message = \
+ "Red cabbage stays red cabbage and wedding dress stays wedding dress"
+ message_translation = \
+ "Blaukraut bleibt Blaukraut und Brautkleid bleibt Brautkleid"
+
+ schema_key_description = "Number of users"
+ schema_key_description_translation = "Anzahl der Nutzer"
+
+ policy_json = """
+ {
+ "policy_definitions": [
+ {
+ 'name': 'MainPolicy',
+ 'type': 'main',
+ 'owners': ['foo@bar.com'],
+ 'schema': {
+ 'properties': {
+ 'default_launch_container': {
+ 'enum': [
+ 'tab',
+ 'window',
+ ],
+ 'type': 'string',
+ },
+ 'users_number': {
+ 'description': '''%s''',
+ 'type': 'integer',
+ },
+ },
+ 'type': 'object',
+ },
+ 'supported_on': ['chrome_os:29-'],
+ 'features': {
+ 'can_be_recommended': True,
+ 'dynamic_refresh': True,
+ },
+ 'example_value': True,
+ 'caption': '''%s''',
+ 'tags': [],
+ 'desc': '''This policy does stuff.'''
+ },
+ ],
+ "policy_atomic_group_definitions": [],
+ "placeholders": [],
+ "messages": {
+ 'message_string_id': {
+ 'desc': '''The description is removed from the grit output''',
+ 'text': '''%s'''
+ }
+ }
+ }""" % (schema_key_description, caption, message)
+
+ # Create translations. The translation IDs are hashed from the English text.
+ caption_id = grit.extern.tclib.GenerateMessageId(caption);
+ message_id = grit.extern.tclib.GenerateMessageId(message);
+ schema_key_description_id = grit.extern.tclib.GenerateMessageId(
+ schema_key_description)
+ policy_xtb = """
+<?xml version="1.0" ?>
+<!DOCTYPE translationbundle>
+<translationbundle lang="de">
+<translation id="%s">%s</translation>
+<translation id="%s">%s</translation>
+<translation id="%s">%s</translation>
+</translationbundle>""" % (caption_id, caption_translation,
+ message_id, message_translation,
+ schema_key_description_id,
+ schema_key_description_translation)
+
+ # Write both to a temp file.
+ tmp_dir_name = tempfile.gettempdir()
+
+ json_file_path = os.path.join(tmp_dir_name, 'test.json')
+ with open(json_file_path, 'w') as f:
+ f.write(policy_json.strip())
+
+ xtb_file_path = os.path.join(tmp_dir_name, 'test.xtb')
+ with open(xtb_file_path, 'w') as f:
+ f.write(policy_xtb.strip())
+
+ # Assemble a test grit tree, similar to policy_templates.grd.
+ grd_text = '''
+ <grit base_dir="." latest_public_release="0" current_release="1" source_lang_id="en">
+ <translations>
+ <file path="%s" lang="de" />
+ </translations>
+ <release seq="1">
+ <structures>
+ <structure name="IDD_POLICY_SOURCE_FILE" file="%s" type="policy_template_metafile" />
+ </structures>
+ </release>
+ </grit>''' % (xtb_file_path, json_file_path)
+ grd_string_io = StringIO(grd_text)
+
+ # Parse the grit tree and load the policies' JSON with a gatherer.
+ grd = grd_reader.Parse(grd_string_io, dir=tmp_dir_name, defines={'_google_chrome': True})
+ grd.SetOutputLanguage('en')
+ grd.RunGatherers()
+
+ # Remove the temp files.
+ os.unlink(xtb_file_path)
+ os.unlink(json_file_path)
+
+ # Run grit with en->de translation.
+ env_lang = 'en'
+ out_lang = 'de'
+ env_defs = {'_google_chrome': '1'}
+
+ grd.SetOutputLanguage(env_lang)
+ grd.SetDefines(env_defs)
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(grd, DummyOutput('policy_templates', out_lang), buf)
+ output = buf.getvalue()
+
+ # Caption and message texts get taken from xtb.
+ # desc is 'translated' to some pseudo-English
+ # 'ThïPïs pôPôlïPïcýPý dôéPôés stüPüff'.
+ expected = u"""{
+ "policy_definitions": [
+ {
+ "caption": "%s",
+ "desc": "Th\xefP\xefs p\xf4P\xf4l\xefP\xefc\xfdP\xfd d\xf4\xe9P\xf4\xe9s st\xfcP\xfcff.",
+ "example_value": true,
+ "features": {"can_be_recommended": true, "dynamic_refresh": true},
+ "name": "MainPolicy",
+ "owners": ["foo@bar.com"],
+ "schema": {
+ "properties": {
+ "default_launch_container": {
+ "enum": [
+ "tab",
+ "window"
+ ],
+ "type": "string"
+ },
+ "users_number": {
+ "description": "%s",
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ },
+ "supported_on": ["chrome_os:29-"],
+ "tags": [],
+ "type": "main"
+ }
+ ],
+ "policy_atomic_group_definitions": [
+ ],
+ "messages": {
+ "message_string_id": {
+ "text": "%s"
+ }
+ }
+
+}""" % (caption_translation, schema_key_description_translation,
+ message_translation)
+ self.assertEqual(expected, output)
+
+
+class DummyOutput(object):
+
+ def __init__(self, type, language):
+ self.type = type
+ self.language = language
+
+ def GetType(self):
+ return self.type
+
+ def GetLanguage(self):
+ return self.language
+
+ def GetOutputFilename(self):
+ return 'hello.gif'
diff --git a/tools/grit/grit/format/rc.py b/tools/grit/grit/format/rc.py
new file mode 100644
index 0000000000..ed32bb809e
--- /dev/null
+++ b/tools/grit/grit/format/rc.py
@@ -0,0 +1,474 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Support for formatting an RC file for compilation.
+'''
+
+from __future__ import print_function
+
+import os
+import re
+from functools import partial
+
+import six
+
+from grit import util
+from grit.node import misc
+
+
+def Format(root, lang='en', output_dir='.'):
+ from grit.node import empty, include, message, structure
+
+ yield _FormatHeader(root, lang, output_dir)
+
+ for item in root.ActiveDescendants():
+ if isinstance(item, empty.MessagesNode):
+ # Write one STRINGTABLE per <messages> container.
+ # This is hacky: it iterates over the children twice.
+ yield 'STRINGTABLE\nBEGIN\n'
+ for subitem in item.ActiveDescendants():
+ if isinstance(subitem, message.MessageNode):
+ with subitem:
+ yield FormatMessage(subitem, lang)
+ yield 'END\n\n'
+ elif isinstance(item, include.IncludeNode):
+ with item:
+ yield FormatInclude(item, lang, output_dir)
+ elif isinstance(item, structure.StructureNode):
+ with item:
+ yield FormatStructure(item, lang, output_dir)
+
+
+'''
+This dictionary defines the language charset pair lookup table, which is used
+for replacing the GRIT expand variables for language info in Product Version
+resource. The key is the language ISO country code, and the value
+is the language and character-set pair, which is a hexadecimal string
+consisting of the concatenation of the language and character-set identifiers.
+The first 4 digit of the value is the hex value of LCID, the remaining
+4 digits is the hex value of character-set id(code page)of the language.
+
+LCID resource: http://msdn.microsoft.com/en-us/library/ms776294.aspx
+Codepage resource: http://www.science.co.il/language/locale-codes.asp
+
+We have defined three GRIT expand_variables to be used in the version resource
+file to set the language info. Here is an example how they should be used in
+the VS_VERSION_INFO section of the resource file to allow GRIT to localize
+the language info correctly according to product locale.
+
+VS_VERSION_INFO VERSIONINFO
+...
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "[GRITVERLANGCHARSETHEX]"
+ BEGIN
+ ...
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", [GRITVERLANGID], [GRITVERCHARSETID]
+ END
+END
+
+'''
+
+_LANGUAGE_CHARSET_PAIR = {
+ # Language neutral LCID, unicode(1200) code page.
+ 'neutral' : '000004b0',
+ # LANG_USER_DEFAULT LCID, unicode(1200) code page.
+ 'userdefault' : '040004b0',
+ 'ar' : '040104e8',
+ 'fi' : '040b04e4',
+ 'ko' : '041203b5',
+ 'es' : '0c0a04e4',
+ 'bg' : '040204e3',
+ # No codepage for filipino, use unicode(1200).
+ 'fil' : '046404e4',
+ 'fr' : '040c04e4',
+ 'lv' : '042604e9',
+ 'sv' : '041d04e4',
+ 'ca' : '040304e4',
+ 'de' : '040704e4',
+ 'lt' : '042704e9',
+ # Do not use! This is only around for backwards
+ # compatibility and will be removed - use fil instead
+ 'tl' : '0c0004b0',
+ 'zh-CN' : '080403a8',
+ 'zh-TW' : '040403b6',
+ 'zh-HK' : '0c0403b6',
+ 'el' : '040804e5',
+ 'no' : '001404e4',
+ 'nb' : '041404e4',
+ 'nn' : '081404e4',
+ 'th' : '041e036a',
+ 'he' : '040d04e7',
+ 'iw' : '040d04e7',
+ 'pl' : '041504e2',
+ 'tr' : '041f04e6',
+ 'hr' : '041a04e4',
+ # No codepage for Hindi, use unicode(1200).
+ 'hi' : '043904b0',
+ 'pt-PT' : '081604e4',
+ 'pt-BR' : '041604e4',
+ 'uk' : '042204e3',
+ 'cs' : '040504e2',
+ 'hu' : '040e04e2',
+ 'ro' : '041804e2',
+ # No codepage for Urdu, use unicode(1200).
+ 'ur' : '042004b0',
+ 'da' : '040604e4',
+ 'is' : '040f04e4',
+ 'ru' : '041904e3',
+ 'vi' : '042a04ea',
+ 'nl' : '041304e4',
+ 'id' : '042104e4',
+ 'sr' : '081a04e2',
+ 'en-GB' : '0809040e',
+ 'it' : '041004e4',
+ 'sk' : '041b04e2',
+ 'et' : '042504e9',
+ 'ja' : '041103a4',
+ 'sl' : '042404e2',
+ 'en' : '040904b0',
+ # LCID for Mexico; Windows does not support L.A. LCID.
+ 'es-419' : '080a04e4',
+ # No codepage for Bengali, use unicode(1200).
+ 'bn' : '044504b0',
+ 'fa' : '042904e8',
+ # No codepage for Gujarati, use unicode(1200).
+ 'gu' : '044704b0',
+ # No codepage for Kannada, use unicode(1200).
+ 'kn' : '044b04b0',
+ # Malay (Malaysia) [ms-MY]
+ 'ms' : '043e04e4',
+ # No codepage for Malayalam, use unicode(1200).
+ 'ml' : '044c04b0',
+ # No codepage for Marathi, use unicode(1200).
+ 'mr' : '044e04b0',
+ # No codepage for Oriya , use unicode(1200).
+ 'or' : '044804b0',
+ # No codepage for Tamil, use unicode(1200).
+ 'ta' : '044904b0',
+ # No codepage for Telugu, use unicode(1200).
+ 'te' : '044a04b0',
+ # No codepage for Amharic, use unicode(1200). >= Vista.
+ 'am' : '045e04b0',
+ 'sw' : '044104e4',
+ 'af' : '043604e4',
+ 'eu' : '042d04e4',
+ 'fr-CA' : '0c0c04e4',
+ 'gl' : '045604e4',
+ # No codepage for Zulu, use unicode(1200).
+ 'zu' : '043504b0',
+ 'fake-bidi' : '040d04e7',
+}
+
+# Language ID resource: http://msdn.microsoft.com/en-us/library/ms776294.aspx
+#
+# There is no appropriate sublang for Spanish (Latin America) [es-419], so we
+# use Mexico. SUBLANG_DEFAULT would incorrectly map to Spain. Unlike other
+# Latin American countries, Mexican Spanish is supported by VERSIONINFO:
+# http://msdn.microsoft.com/en-us/library/aa381058.aspx
+
+_LANGUAGE_DIRECTIVE_PAIR = {
+ 'neutral' : 'LANG_NEUTRAL, SUBLANG_NEUTRAL',
+ 'userdefault' : 'LANG_NEUTRAL, SUBLANG_DEFAULT',
+ 'ar' : 'LANG_ARABIC, SUBLANG_DEFAULT',
+ 'fi' : 'LANG_FINNISH, SUBLANG_DEFAULT',
+ 'ko' : 'LANG_KOREAN, SUBLANG_KOREAN',
+ 'es' : 'LANG_SPANISH, SUBLANG_SPANISH_MODERN',
+ 'bg' : 'LANG_BULGARIAN, SUBLANG_DEFAULT',
+ # LANG_FILIPINO (100) not in VC 7 winnt.h.
+ 'fil' : '100, SUBLANG_DEFAULT',
+ 'fr' : 'LANG_FRENCH, SUBLANG_FRENCH',
+ 'lv' : 'LANG_LATVIAN, SUBLANG_DEFAULT',
+ 'sv' : 'LANG_SWEDISH, SUBLANG_SWEDISH',
+ 'ca' : 'LANG_CATALAN, SUBLANG_DEFAULT',
+ 'de' : 'LANG_GERMAN, SUBLANG_GERMAN',
+ 'lt' : 'LANG_LITHUANIAN, SUBLANG_LITHUANIAN',
+ # Do not use! See above.
+ 'tl' : 'LANG_NEUTRAL, SUBLANG_DEFAULT',
+ 'zh-CN' : 'LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED',
+ 'zh-TW' : 'LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL',
+ 'zh-HK' : 'LANG_CHINESE, SUBLANG_CHINESE_HONGKONG',
+ 'el' : 'LANG_GREEK, SUBLANG_DEFAULT',
+ 'no' : 'LANG_NORWEGIAN, SUBLANG_DEFAULT',
+ 'nb' : 'LANG_NORWEGIAN, SUBLANG_NORWEGIAN_BOKMAL',
+ 'nn' : 'LANG_NORWEGIAN, SUBLANG_NORWEGIAN_NYNORSK',
+ 'th' : 'LANG_THAI, SUBLANG_DEFAULT',
+ 'he' : 'LANG_HEBREW, SUBLANG_DEFAULT',
+ 'iw' : 'LANG_HEBREW, SUBLANG_DEFAULT',
+ 'pl' : 'LANG_POLISH, SUBLANG_DEFAULT',
+ 'tr' : 'LANG_TURKISH, SUBLANG_DEFAULT',
+ 'hr' : 'LANG_CROATIAN, SUBLANG_DEFAULT',
+ 'hi' : 'LANG_HINDI, SUBLANG_DEFAULT',
+ 'pt-PT' : 'LANG_PORTUGUESE, SUBLANG_PORTUGUESE',
+ 'pt-BR' : 'LANG_PORTUGUESE, SUBLANG_DEFAULT',
+ 'uk' : 'LANG_UKRAINIAN, SUBLANG_DEFAULT',
+ 'cs' : 'LANG_CZECH, SUBLANG_DEFAULT',
+ 'hu' : 'LANG_HUNGARIAN, SUBLANG_DEFAULT',
+ 'ro' : 'LANG_ROMANIAN, SUBLANG_DEFAULT',
+ 'ur' : 'LANG_URDU, SUBLANG_DEFAULT',
+ 'da' : 'LANG_DANISH, SUBLANG_DEFAULT',
+ 'is' : 'LANG_ICELANDIC, SUBLANG_DEFAULT',
+ 'ru' : 'LANG_RUSSIAN, SUBLANG_DEFAULT',
+ 'vi' : 'LANG_VIETNAMESE, SUBLANG_DEFAULT',
+ 'nl' : 'LANG_DUTCH, SUBLANG_DEFAULT',
+ 'id' : 'LANG_INDONESIAN, SUBLANG_DEFAULT',
+ 'sr' : 'LANG_SERBIAN, SUBLANG_SERBIAN_LATIN',
+ 'en-GB' : 'LANG_ENGLISH, SUBLANG_ENGLISH_UK',
+ 'it' : 'LANG_ITALIAN, SUBLANG_DEFAULT',
+ 'sk' : 'LANG_SLOVAK, SUBLANG_DEFAULT',
+ 'et' : 'LANG_ESTONIAN, SUBLANG_DEFAULT',
+ 'ja' : 'LANG_JAPANESE, SUBLANG_DEFAULT',
+ 'sl' : 'LANG_SLOVENIAN, SUBLANG_DEFAULT',
+ 'en' : 'LANG_ENGLISH, SUBLANG_ENGLISH_US',
+ # No L.A. sublang exists.
+ 'es-419' : 'LANG_SPANISH, SUBLANG_SPANISH_MEXICAN',
+ 'bn' : 'LANG_BENGALI, SUBLANG_DEFAULT',
+ 'fa' : 'LANG_PERSIAN, SUBLANG_DEFAULT',
+ 'gu' : 'LANG_GUJARATI, SUBLANG_DEFAULT',
+ 'kn' : 'LANG_KANNADA, SUBLANG_DEFAULT',
+ 'ms' : 'LANG_MALAY, SUBLANG_DEFAULT',
+ 'ml' : 'LANG_MALAYALAM, SUBLANG_DEFAULT',
+ 'mr' : 'LANG_MARATHI, SUBLANG_DEFAULT',
+ 'or' : 'LANG_ORIYA, SUBLANG_DEFAULT',
+ 'ta' : 'LANG_TAMIL, SUBLANG_DEFAULT',
+ 'te' : 'LANG_TELUGU, SUBLANG_DEFAULT',
+ 'am' : 'LANG_AMHARIC, SUBLANG_DEFAULT',
+ 'sw' : 'LANG_SWAHILI, SUBLANG_DEFAULT',
+ 'af' : 'LANG_AFRIKAANS, SUBLANG_DEFAULT',
+ 'eu' : 'LANG_BASQUE, SUBLANG_DEFAULT',
+ 'fr-CA' : 'LANG_FRENCH, SUBLANG_FRENCH_CANADIAN',
+ 'gl' : 'LANG_GALICIAN, SUBLANG_DEFAULT',
+ 'zu' : 'LANG_ZULU, SUBLANG_DEFAULT',
+ 'pa' : 'LANG_PUNJABI, SUBLANG_PUNJABI_INDIA',
+ 'sa' : 'LANG_SANSKRIT, SUBLANG_SANSKRIT_INDIA',
+ 'si' : 'LANG_SINHALESE, SUBLANG_SINHALESE_SRI_LANKA',
+ 'ne' : 'LANG_NEPALI, SUBLANG_NEPALI_NEPAL',
+ 'ti' : 'LANG_TIGRIGNA, SUBLANG_TIGRIGNA_ERITREA',
+ 'fake-bidi' : 'LANG_HEBREW, SUBLANG_DEFAULT',
+}
+
+# A note on 'no-specific-language' in the following few functions:
+# Some build systems may wish to call GRIT to scan for dependencies in
+# a language-agnostic way, and can then specify this fake language as
+# the output context. It should never be used when output is actually
+# being generated.
+
+def GetLangCharsetPair(language):
+ if language in _LANGUAGE_CHARSET_PAIR:
+ return _LANGUAGE_CHARSET_PAIR[language]
+ if language != 'no-specific-language':
+ print('Warning:GetLangCharsetPair() found undefined language %s' % language)
+ return ''
+
+def GetLangDirectivePair(language):
+ if language in _LANGUAGE_DIRECTIVE_PAIR:
+ return _LANGUAGE_DIRECTIVE_PAIR[language]
+
+ # We don't check for 'no-specific-language' here because this
+ # function should only get called when output is being formatted,
+ # and at that point we would not want to get
+ # 'no-specific-language' passed as the language.
+ print('Warning:GetLangDirectivePair() found undefined language %s' % language)
+ return 'unknown language: see tools/grit/format/rc.py'
+
+def GetLangIdHex(language):
+ if language in _LANGUAGE_CHARSET_PAIR:
+ langcharset = _LANGUAGE_CHARSET_PAIR[language]
+ lang_id = '0x' + langcharset[0:4]
+ return lang_id
+ if language != 'no-specific-language':
+ print('Warning:GetLangIdHex() found undefined language %s' % language)
+ return ''
+
+
+def GetCharsetIdDecimal(language):
+ if language in _LANGUAGE_CHARSET_PAIR:
+ langcharset = _LANGUAGE_CHARSET_PAIR[language]
+ charset_decimal = int(langcharset[4:], 16)
+ return str(charset_decimal)
+ if language != 'no-specific-language':
+ print('Warning:GetCharsetIdDecimal() found undefined language %s' % language)
+ return ''
+
+
+def GetUnifiedLangCode(language) :
+ r = re.compile('([a-z]{1,2})_([a-z]{1,2})')
+ if r.match(language) :
+ underscore = language.find('_')
+ return language[0:underscore] + '-' + language[underscore + 1:].upper()
+ return language
+
+
+def RcSubstitutions(substituter, lang):
+ '''Add language-based substitutions for Rc files to the substitutor.'''
+ unified_lang_code = GetUnifiedLangCode(lang)
+ substituter.AddSubstitutions({
+ 'GRITVERLANGCHARSETHEX': GetLangCharsetPair(unified_lang_code),
+ 'GRITVERLANGID': GetLangIdHex(unified_lang_code),
+ 'GRITVERCHARSETID': GetCharsetIdDecimal(unified_lang_code)})
+
+
+def _FormatHeader(root, lang, output_dir):
+ '''Returns the required preamble for RC files.'''
+ assert isinstance(lang, six.string_types)
+ assert isinstance(root, misc.GritNode)
+ # Find the location of the resource header file, so that we can include
+ # it.
+ resource_header = 'resource.h' # fall back to this
+ language_directive = ''
+ for output in root.GetOutputFiles():
+ if output.attrs['type'] == 'rc_header':
+ resource_header = os.path.abspath(output.GetOutputFilename())
+ resource_header = util.MakeRelativePath(output_dir, resource_header)
+ if output.attrs['lang'] != lang:
+ continue
+ if output.attrs['language_section'] == '':
+ # If no language_section is requested, no directive is added
+ # (Used when the generated rc will be included from another rc
+ # file that will have the appropriate language directive)
+ language_directive = ''
+ elif output.attrs['language_section'] == 'neutral':
+ # If a neutral language section is requested (default), add a
+ # neutral language directive
+ language_directive = 'LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL'
+ elif output.attrs['language_section'] == 'lang':
+ language_directive = 'LANGUAGE %s' % GetLangDirectivePair(lang)
+ resource_header = resource_header.replace('\\', '\\\\')
+ return '''// This file is automatically generated by GRIT. Do not edit.
+
+#include "%s"
+#include <winresrc.h>
+#ifdef IDC_STATIC
+#undef IDC_STATIC
+#endif
+#define IDC_STATIC (-1)
+
+%s
+
+
+''' % (resource_header, language_directive)
+# end _FormatHeader() function
+
+
+def FormatMessage(item, lang):
+ '''Returns a single message of a string table.'''
+ message = item.ws_at_start + item.Translate(lang) + item.ws_at_end
+ # Escape quotation marks (RC format uses doubling-up
+ message = message.replace('"', '""')
+ # Replace linebreaks with a \n escape
+ message = util.LINEBREAKS.sub(r'\\n', message)
+ if hasattr(item.GetRoot(), 'GetSubstituter'):
+ substituter = item.GetRoot().GetSubstituter()
+ message = substituter.Substitute(message)
+
+ name_attr = item.GetTextualIds()[0]
+
+ return ' %-15s "%s"\n' % (name_attr, message)
+
+
+def _FormatSection(item, lang, output_dir):
+ '''Writes out an .rc file section.'''
+ assert isinstance(lang, six.string_types)
+ from grit.node import structure
+ assert isinstance(item, structure.StructureNode)
+
+ if item.IsExcludedFromRc():
+ return ''
+
+ text = item.gatherer.Translate(
+ lang, skeleton_gatherer=item.GetSkeletonGatherer(),
+ pseudo_if_not_available=item.PseudoIsAllowed(),
+ fallback_to_english=item.ShouldFallbackToEnglish()) + '\n\n'
+
+ # Replace the language expand_variables in version rc info.
+ if item.ExpandVariables() and hasattr(item.GetRoot(), 'GetSubstituter'):
+ substituter = item.GetRoot().GetSubstituter()
+ text = substituter.Substitute(text)
+ return text
+
+
+def FormatInclude(item, lang, output_dir, type=None, process_html=False):
+ '''Formats an item that is included in an .rc file (e.g. an ICON).
+
+ Args:
+ item: an IncludeNode or StructureNode
+ lang, output_dir: standard formatter parameters
+ type: .rc file resource type, e.g. 'ICON' (ignored unless item is a
+ StructureNode)
+ process_html: False/True (ignored unless item is a StructureNode)
+ '''
+ assert isinstance(lang, six.string_types)
+ from grit.node import structure
+ from grit.node import include
+ assert isinstance(item, (structure.StructureNode, include.IncludeNode))
+
+ if isinstance(item, include.IncludeNode):
+ type = item.attrs['type'].upper()
+ process_html = item.attrs['flattenhtml'] == 'true'
+ filename_only = item.attrs['filenameonly'] == 'true'
+ relative_path = item.attrs['relativepath'] == 'true'
+ else:
+ assert (isinstance(item, structure.StructureNode) and item.attrs['type'] in
+ ['admin_template', 'chrome_html', 'chrome_scaled_image',
+ 'tr_html', 'txt'])
+ filename_only = False
+ relative_path = False
+
+ # By default, we use relative pathnames to included resources so that
+ # sharing the resulting .rc files is possible.
+ #
+ # The FileForLanguage() Function has the side effect of generating the file
+ # if needed (e.g. if it is an HTML file include).
+ file_for_lang = item.FileForLanguage(lang, output_dir)
+ if file_for_lang is None:
+ return ''
+
+ filename = os.path.abspath(file_for_lang)
+ if process_html:
+ filename = item.Process(output_dir)
+ elif filename_only:
+ filename = os.path.basename(filename)
+ elif relative_path:
+ filename = util.MakeRelativePath(output_dir, filename)
+
+ filename = filename.replace('\\', '\\\\') # escape for the RC format
+
+ if isinstance(item, structure.StructureNode) and item.IsExcludedFromRc():
+ return ''
+
+ name = item.attrs['name']
+ item_id = item.GetRoot().GetIdMap()[name]
+ return '// ID: %d\n%-18s %-18s "%s"\n' % (item_id, name, type, filename)
+
+
+def _DoNotFormat(item, lang, output_dir):
+ return ''
+
+
+# Formatter instance to use for each type attribute
+# when formatting Structure nodes.
+_STRUCTURE_FORMATTERS = {
+ 'accelerators' : _FormatSection,
+ 'dialog' : _FormatSection,
+ 'menu' : _FormatSection,
+ 'rcdata' : _FormatSection,
+ 'version' : _FormatSection,
+ 'admin_template' : partial(FormatInclude, type='ADM'),
+ 'chrome_html' : partial(FormatInclude, type='BINDATA',
+ process_html=True),
+ 'chrome_scaled_image' : partial(FormatInclude, type='BINDATA'),
+ 'tr_html' : partial(FormatInclude, type='HTML'),
+ 'txt' : partial(FormatInclude, type='TXT'),
+ 'policy_template_metafile': _DoNotFormat,
+}
+
+
+def FormatStructure(item, lang, output_dir):
+ formatter = _STRUCTURE_FORMATTERS[item.attrs['type']]
+ return formatter(item, lang, output_dir)
diff --git a/tools/grit/grit/format/rc_header.py b/tools/grit/grit/format/rc_header.py
new file mode 100644
index 0000000000..ea2c217f53
--- /dev/null
+++ b/tools/grit/grit/format/rc_header.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Item formatters for RC headers.
+'''
+
+from __future__ import print_function
+
+
+def Format(root, lang='en', output_dir='.'):
+ yield '''\
+// This file is automatically generated by GRIT. Do not edit.
+
+#pragma once
+'''
+ # Check for emit nodes under the rc_header. If any emit node
+ # is present, we assume it means the GRD file wants to override
+ # the default header, with no includes.
+ default_includes = ['#include <atlres.h>', '']
+ emit_lines = []
+ for output_node in root.GetOutputFiles():
+ if output_node.GetType() == 'rc_header':
+ for child in output_node.children:
+ if child.name == 'emit' and child.attrs['emit_type'] == 'prepend':
+ emit_lines.append(child.GetCdata())
+ for line in emit_lines or default_includes:
+ yield line + '\n'
+ if root.IsWhitelistSupportEnabled():
+ yield '#include "ui/base/resource/whitelist.h"\n'
+ for line in FormatDefines(root):
+ yield line
+
+
+def FormatDefines(root):
+ '''Yields #define SYMBOL 1234 lines.
+
+ Args:
+ root: A GritNode.
+ '''
+ tids = root.GetIdMap()
+ rc_header_format = '#define {0} {1}\n'
+ if root.IsWhitelistSupportEnabled():
+ rc_header_format = '#define {0} (::ui::WhitelistedResource<{1}>(), {1})\n'
+ for item in root.ActiveDescendants():
+ with item:
+ for tid in item.GetTextualIds():
+ yield rc_header_format.format(tid, tids[tid])
diff --git a/tools/grit/grit/format/rc_header_unittest.py b/tools/grit/grit/format/rc_header_unittest.py
new file mode 100644
index 0000000000..eed4d70a99
--- /dev/null
+++ b/tools/grit/grit/format/rc_header_unittest.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for the rc_header formatter'''
+
+# GRD samples exceed the 80 character limit.
+# pylint: disable-msg=C6310
+
+from __future__ import print_function
+
+import os
+import sys
+import unittest
+
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from grit import util
+from grit.format import rc_header
+
+
+class RcHeaderFormatterUnittest(unittest.TestCase):
+ def FormatAll(self, grd):
+ output = rc_header.FormatDefines(grd)
+ return ''.join(output).replace(' ', '')
+
+ def testFormatter(self):
+ grd = util.ParseGrdForUnittest('''
+ <includes first_id="300" comment="bingo">
+ <include type="gif" name="ID_LOGO" file="images/logo.gif" />
+ </includes>
+ <messages first_id="10000">
+ <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+ Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+ </message>
+ <message name="IDS_BONGO">
+ Bongo!
+ </message>
+ </messages>
+ <structures>
+ <structure type="dialog" name="IDD_NARROW_DIALOG" file="rc_files/dialogs.rc" />
+ <structure type="version" name="VS_VERSION_INFO" file="rc_files/version.rc" />
+ </structures>''')
+ output = self.FormatAll(grd)
+ self.failUnless(output.count('IDS_GREETING10000'))
+ self.failUnless(output.count('ID_LOGO300'))
+
+ def testOnlyDefineResourcesThatSatisfyOutputCondition(self):
+ grd = util.ParseGrdForUnittest('''
+ <includes first_id="300" comment="bingo">
+ <include type="gif" name="ID_LOGO" file="images/logo.gif" />
+ </includes>
+ <messages first_id="10000">
+ <message name="IDS_FIRSTPRESENTSTRING" desc="Present in .rc file.">
+ I will appear in the .rc file.
+ </message>
+ <if expr="False"> <!--Do not include in the .rc files until used.-->
+ <message name="IDS_MISSINGSTRING" desc="Not present in .rc file.">
+ I will not appear in the .rc file.
+ </message>
+ </if>
+ <if expr="lang != 'es'">
+ <message name="IDS_LANGUAGESPECIFICSTRING" desc="Present in .rc file.">
+ Hello.
+ </message>
+ </if>
+ <if expr="lang == 'es'">
+ <message name="IDS_LANGUAGESPECIFICSTRING" desc="Present in .rc file.">
+ Hola.
+ </message>
+ </if>
+ <message name="IDS_THIRDPRESENTSTRING" desc="Present in .rc file.">
+ I will also appear in the .rc file.
+ </message>
+ </messages>''')
+ output = self.FormatAll(grd)
+ self.failUnless(output.count('IDS_FIRSTPRESENTSTRING10000'))
+ self.failIf(output.count('IDS_MISSINGSTRING'))
+ self.failUnless(output.count('IDS_LANGUAGESPECIFICSTRING10002'))
+ self.failUnless(output.count('IDS_THIRDPRESENTSTRING10003'))
+
+ def testEmit(self):
+ grd = util.ParseGrdForUnittest('''
+ <outputs>
+ <output type="rc_all" filename="dummy">
+ <emit emit_type="prepend">Wrong</emit>
+ </output>
+ <if expr="False">
+ <output type="rc_header" filename="dummy">
+ <emit emit_type="prepend">No</emit>
+ </output>
+ </if>
+ <output type="rc_header" filename="dummy">
+ <emit emit_type="append">Error</emit>
+ </output>
+ <output type="rc_header" filename="dummy">
+ <emit emit_type="prepend">Bingo</emit>
+ </output>
+ </outputs>''')
+ output = ''.join(rc_header.Format(grd, 'en', '.'))
+ output = util.StripBlankLinesAndComments(output)
+ self.assertEqual('#pragma once\nBingo', output)
+
+ def testRcHeaderFormat(self):
+ grd = util.ParseGrdForUnittest('''
+ <includes first_id="300" comment="bingo">
+ <include type="gif" name="IDR_LOGO" file="images/logo.gif" />
+ </includes>
+ <messages first_id="10000">
+ <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+ Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+ </message>
+ <message name="IDS_BONGO">
+ Bongo!
+ </message>
+ </messages>''')
+
+ # Using the default settings.
+ output = rc_header.FormatDefines(grd)
+ self.assertEqual(('#define IDR_LOGO 300\n'
+ '#define IDS_GREETING 10000\n'
+ '#define IDS_BONGO 10001\n'), ''.join(output))
+
+ # Using resource whitelist support.
+ grd.SetWhitelistSupportEnabled(True)
+ output = rc_header.FormatDefines(grd)
+ self.assertEqual(('#define IDR_LOGO '
+ '(::ui::WhitelistedResource<300>(), 300)\n'
+ '#define IDS_GREETING '
+ '(::ui::WhitelistedResource<10000>(), 10000)\n'
+ '#define IDS_BONGO '
+ '(::ui::WhitelistedResource<10001>(), 10001)\n'),
+ ''.join(output))
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/rc_unittest.py b/tools/grit/grit/format/rc_unittest.py
new file mode 100644
index 0000000000..d23f063596
--- /dev/null
+++ b/tools/grit/grit/format/rc_unittest.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.rc'''
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import tempfile
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit import util
+from grit.node import structure
+from grit.tool import build
+
+
+_PREAMBLE = '''\
+#include "resource.h"
+#include <winresrc.h>
+#ifdef IDC_STATIC
+#undef IDC_STATIC
+#endif
+#define IDC_STATIC (-1)
+'''
+
+
+class DummyOutput(object):
+ def __init__(self, type, language, file = 'hello.gif'):
+ self.type = type
+ self.language = language
+ self.file = file
+
+ def GetType(self):
+ return self.type
+
+ def GetLanguage(self):
+ return self.language
+
+ def GetOutputFilename(self):
+ return self.file
+
+
+class FormatRcUnittest(unittest.TestCase):
+ def testMessages(self):
+ root = util.ParseGrdForUnittest("""
+ <messages>
+ <message name="IDS_BTN_GO" desc="Button text" meaning="verb">Go!</message>
+ <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+ Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+ </message>
+ <message name="BONGO" desc="Flippo nippo">
+ Howdie "Mr. Elephant", how are you doing? '''
+ </message>
+ <message name="IDS_WITH_LINEBREAKS">
+Good day sir,
+I am a bee
+Sting sting
+ </message>
+ </messages>
+ """)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+ self.assertEqual(_PREAMBLE + u'''\
+STRINGTABLE
+BEGIN
+ IDS_BTN_GO "Go!"
+ IDS_GREETING "Hello %s, how are you doing today?"
+ BONGO "Howdie ""Mr. Elephant"", how are you doing? "
+ IDS_WITH_LINEBREAKS "Good day sir,\\nI am a bee\\nSting sting"
+END''', output)
+
+ def testRcSection(self):
+ root = util.ParseGrdForUnittest(r'''
+ <structures>
+ <structure type="menu" name="IDC_KLONKMENU" file="grit\testdata\klonk.rc" encoding="utf-16" />
+ <structure type="dialog" name="IDD_ABOUTBOX" file="grit\testdata\klonk.rc" encoding="utf-16" />
+ <structure type="version" name="VS_VERSION_INFO" file="grit\testdata\klonk.rc" encoding="utf-16" />
+ </structures>''')
+ root.SetOutputLanguage('en')
+ root.RunGatherers()
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+ expected = _PREAMBLE + u'''\
+IDC_KLONKMENU MENU
+BEGIN
+ POPUP "&File"
+ BEGIN
+ MENUITEM "E&xit", IDM_EXIT
+ MENUITEM "This be ""Klonk"" me like", ID_FILE_THISBE
+ POPUP "gonk"
+ BEGIN
+ MENUITEM "Klonk && is [good]", ID_GONK_KLONKIS
+ END
+ END
+ POPUP "&Help"
+ BEGIN
+ MENUITEM "&About ...", IDM_ABOUT
+ END
+END
+
+IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+ ICON IDI_KLONK,IDC_MYICON,14,9,20,20
+ LTEXT "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+ SS_NOPREFIX
+ LTEXT "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+ DEFPUSHBUTTON "OK",IDOK,195,6,30,11,WS_GROUP
+ CONTROL "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+ BS_AUTORADIOBUTTON,46,51,84,10
+END
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,0,0,1
+ PRODUCTVERSION 1,0,0,1
+ FILEFLAGSMASK 0x17L
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x4L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904b0"
+ BEGIN
+ VALUE "FileDescription", "klonk Application"
+ VALUE "FileVersion", "1, 0, 0, 1"
+ VALUE "InternalName", "klonk"
+ VALUE "LegalCopyright", "Copyright (C) 2005"
+ VALUE "OriginalFilename", "klonk.exe"
+ VALUE "ProductName", " klonk Application"
+ VALUE "ProductVersion", "1, 0, 0, 1"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1200
+ END
+END'''.strip()
+ for expected_line, output_line in zip(expected.split(), output.split()):
+ self.assertEqual(expected_line, output_line)
+
+ def testRcIncludeStructure(self):
+ root = util.ParseGrdForUnittest('''
+ <structures>
+ <structure type="tr_html" name="IDR_HTML" file="bingo.html"/>
+ <structure type="tr_html" name="IDR_HTML2" file="bingo2.html"/>
+ </structures>''', base_dir = '/temp')
+ # We do not run gatherers as it is not needed and wouldn't find the file
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+ expected = (_PREAMBLE +
+ u'IDR_HTML HTML "%s"\n'
+ u'IDR_HTML2 HTML "%s"'
+ % (util.normpath('/temp/bingo.html').replace('\\', '\\\\'),
+ util.normpath('/temp/bingo2.html').replace('\\', '\\\\')))
+ # hackety hack to work on win32&lin
+ output = re.sub(r'"[c-zC-Z]:', '"', output)
+ self.assertEqual(expected, output)
+
+ def testRcIncludeFile(self):
+ root = util.ParseGrdForUnittest('''
+ <includes>
+ <include type="TXT" name="TEXT_ONE" file="bingo.txt"/>
+ <include type="TXT" name="TEXT_TWO" file="bingo2.txt" filenameonly="true" />
+ </includes>''', base_dir = '/temp')
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+ expected = (_PREAMBLE +
+ u'TEXT_ONE TXT "%s"\n'
+ u'TEXT_TWO TXT "%s"'
+ % (util.normpath('/temp/bingo.txt').replace('\\', '\\\\'),
+ 'bingo2.txt'))
+ # hackety hack to work on win32&lin
+ output = re.sub(r'"[c-zC-Z]:', '"', output)
+ self.assertEqual(expected, output)
+
+ def testRcIncludeFlattenedHtmlFile(self):
+ input_file = util.PathFromRoot('grit/testdata/include_test.html')
+ output_file = '%s/HTML_FILE1_include_test.html' % tempfile.gettempdir()
+ root = util.ParseGrdForUnittest('''
+ <includes>
+ <include name="HTML_FILE1" flattenhtml="true" file="%s" type="BINDATA" />
+ </includes>''' % input_file)
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en', output_file),
+ buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+
+ expected = (_PREAMBLE +
+ u'HTML_FILE1 BINDATA "HTML_FILE1_include_test.html"')
+ # hackety hack to work on win32&lin
+ output = re.sub(r'"[c-zC-Z]:', '"', output)
+ self.assertEqual(expected, output)
+
+ file_contents = util.ReadFile(output_file, 'utf-8')
+
+ # Check for the content added by the <include> tag.
+ self.failUnless(file_contents.find('Hello Include!') != -1)
+ # Check for the content that was removed by if tag.
+ self.failUnless(file_contents.find('should be removed') == -1)
+ # Check for the content that was kept in place by if.
+ self.failUnless(file_contents.find('should be kept') != -1)
+ self.failUnless(file_contents.find('in the middle...') != -1)
+ self.failUnless(file_contents.find('at the end...') != -1)
+ # Check for nested content that was kept
+ self.failUnless(file_contents.find('nested true should be kept') != -1)
+ self.failUnless(file_contents.find('silbing true should be kept') != -1)
+ # Check for removed "<if>" and "</if>" tags.
+ self.failUnless(file_contents.find('<if expr=') == -1)
+ self.failUnless(file_contents.find('</if>') == -1)
+ os.remove(output_file)
+
+ def testStructureNodeOutputfile(self):
+ input_file = util.PathFromRoot('grit/testdata/simple.html')
+ root = util.ParseGrdForUnittest('''
+ <structures>
+ <structure type="tr_html" name="IDR_HTML" file="%s" />
+ </structures>''' % input_file)
+ struct, = root.GetChildrenOfType(structure.StructureNode)
+ # We must run the gatherer since we'll be wanting the translation of the
+ # file. The file exists in the location pointed to.
+ root.SetOutputLanguage('en')
+ root.RunGatherers()
+
+ output_dir = tempfile.gettempdir()
+ en_file = struct.FileForLanguage('en', output_dir)
+ self.failUnless(en_file == input_file)
+ fr_file = struct.FileForLanguage('fr', output_dir)
+ self.failUnless(fr_file == os.path.join(output_dir, 'fr_simple.html'))
+
+ contents = util.ReadFile(fr_file, 'utf-8')
+
+ self.failUnless(contents.find('<p>') != -1) # should contain the markup
+ self.failUnless(contents.find('Hello!') == -1) # should be translated
+ os.remove(fr_file)
+
+ def testChromeHtmlNodeOutputfile(self):
+ input_file = util.PathFromRoot('grit/testdata/chrome_html.html')
+ output_file = '%s/HTML_FILE1_chrome_html.html' % tempfile.gettempdir()
+ root = util.ParseGrdForUnittest('''
+ <structures>
+ <structure type="chrome_html" name="HTML_FILE1" file="%s" flattenhtml="true" />
+ </structures>''' % input_file)
+ struct, = root.GetChildrenOfType(structure.StructureNode)
+ struct.gatherer.SetDefines({'scale_factors': '2x'})
+ # We must run the gatherers since we'll be wanting the chrome_html output.
+ # The file exists in the location pointed to.
+ root.SetOutputLanguage('en')
+ root.RunGatherers()
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en', output_file),
+ buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+ expected = (_PREAMBLE +
+ u'HTML_FILE1 BINDATA "HTML_FILE1_chrome_html.html"')
+ # hackety hack to work on win32&lin
+ output = re.sub(r'"[c-zC-Z]:', '"', output)
+ self.assertEqual(expected, output)
+
+ file_contents = util.ReadFile(output_file, 'utf-8')
+
+ # Check for the content added by the <include> tag.
+ self.failUnless(file_contents.find('Hello Include!') != -1)
+ # Check for inserted -webkit-image-set.
+ self.failUnless(file_contents.find('content: -webkit-image-set') != -1)
+ os.remove(output_file)
+
+ def testSubstitutionHtml(self):
+ input_file = util.PathFromRoot('grit/testdata/toolbar_about.html')
+ root = grd_reader.Parse(StringIO('''<?xml version="1.0" encoding="UTF-8"?>
+ <grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+ <release seq="1" allow_pseudo="False">
+ <structures fallback_to_english="True">
+ <structure type="tr_html" name="IDR_HTML" file="%s" expand_variables="true"/>
+ </structures>
+ </release>
+ </grit>
+ ''' % input_file), util.PathFromRoot('.'))
+ root.SetOutputLanguage('ar')
+ # We must run the gatherers since we'll be wanting the translation of the
+ # file. The file exists in the location pointed to.
+ root.RunGatherers()
+
+ output_dir = tempfile.gettempdir()
+ struct, = root.GetChildrenOfType(structure.StructureNode)
+ ar_file = struct.FileForLanguage('ar', output_dir)
+ self.failUnless(ar_file == os.path.join(output_dir,
+ 'ar_toolbar_about.html'))
+
+ contents = util.ReadFile(ar_file, 'utf-8')
+
+ self.failUnless(contents.find('dir="RTL"') != -1)
+ os.remove(ar_file)
+
+ def testFallbackToEnglish(self):
+ root = util.ParseGrdForUnittest(r'''
+ <structures fallback_to_english="True">
+ <structure type="dialog" name="IDD_ABOUTBOX" file="grit\testdata\klonk.rc" encoding="utf-16" />
+ </structures>''', base_dir=util.PathFromRoot('.'))
+ root.SetOutputLanguage('en')
+ root.RunGatherers()
+
+ buf = StringIO()
+ formatter = build.RcBuilder.ProcessNode(
+ root, DummyOutput('rc_all', 'bingobongo'), buf)
+ output = util.StripBlankLinesAndComments(buf.getvalue())
+ self.assertEqual(_PREAMBLE + '''\
+IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+ ICON IDI_KLONK,IDC_MYICON,14,9,20,20
+ LTEXT "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+ SS_NOPREFIX
+ LTEXT "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+ DEFPUSHBUTTON "OK",IDOK,195,6,30,11,WS_GROUP
+ CONTROL "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+ BS_AUTORADIOBUTTON,46,51,84,10
+END''', output)
+
+
+ def testSubstitutionRc(self):
+ root = grd_reader.Parse(StringIO(r'''<?xml version="1.0" encoding="UTF-8"?>
+ <grit latest_public_release="2" source_lang_id="en-US" current_release="3"
+ base_dir=".">
+ <outputs>
+ <output lang="en" type="rc_all" filename="grit\testdata\klonk_resources.rc"/>
+ </outputs>
+ <release seq="1" allow_pseudo="False">
+ <structures>
+ <structure type="menu" name="IDC_KLONKMENU"
+ file="grit\testdata\klonk.rc" encoding="utf-16"
+ expand_variables="true" />
+ </structures>
+ <messages>
+ <message name="good" sub_variable="true">
+ excellent
+ </message>
+ </messages>
+ </release>
+ </grit>
+ '''), util.PathFromRoot('.'))
+ root.SetOutputLanguage('en')
+ root.RunGatherers()
+
+ buf = StringIO()
+ build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+ output = buf.getvalue()
+ self.assertEqual('''
+// This file is automatically generated by GRIT. Do not edit.
+
+#include "resource.h"
+#include <winresrc.h>
+#ifdef IDC_STATIC
+#undef IDC_STATIC
+#endif
+#define IDC_STATIC (-1)
+
+LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
+
+
+IDC_KLONKMENU MENU
+BEGIN
+ POPUP "&File"
+ BEGIN
+ MENUITEM "E&xit", IDM_EXIT
+ MENUITEM "This be ""Klonk"" me like", ID_FILE_THISBE
+ POPUP "gonk"
+ BEGIN
+ MENUITEM "Klonk && is excellent", ID_GONK_KLONKIS
+ END
+ END
+ POPUP "&Help"
+ BEGIN
+ MENUITEM "&About ...", IDM_ABOUT
+ END
+END
+
+STRINGTABLE
+BEGIN
+ good "excellent"
+END
+'''.strip(), output.strip())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/format/resource_map.py b/tools/grit/grit/format/resource_map.py
new file mode 100644
index 0000000000..95a8b83160
--- /dev/null
+++ b/tools/grit/grit/format/resource_map.py
@@ -0,0 +1,159 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''This file contains item formatters for resource_map_header and
+resource_map_source files. A resource map is a mapping between resource names
+(string) and the internal resource ID.'''
+
+from __future__ import print_function
+
+import os
+from functools import partial
+
+from grit import util
+
+
+def GetFormatter(type):
+ if type == 'resource_map_header':
+ return _FormatHeader
+ if type == 'resource_file_map_source':
+ return partial(_FormatSource, _GetItemPath)
+ if type == 'resource_map_source':
+ return partial(_FormatSource, _GetItemName)
+
+
+def GetMapName(root):
+ '''Get the name of the resource map based on the header file name. E.g.,
+ if our header filename is theme_resources.h, we name our resource map
+ kThemeResourcesMap.
+
+ |root| is the grd file root.'''
+ outputs = root.GetOutputFiles()
+ rc_header_file = None
+ for output in outputs:
+ if 'rc_header' == output.GetType():
+ rc_header_file = output.GetFilename()
+ if not rc_header_file:
+ raise Exception('unable to find resource header filename')
+ filename = os.path.splitext(os.path.split(rc_header_file)[1])[0]
+ filename = filename[0].upper() + filename[1:]
+ while True:
+ pos = filename.find('_')
+ if pos == -1 or pos >= len(filename):
+ break
+ filename = filename[:pos] + filename[pos + 1].upper() + filename[pos + 2:]
+ return 'k' + filename
+
+
+def _FormatHeader(root, lang='en', output_dir='.'):
+ '''Create the header file for the resource mapping. This file just declares
+ an array of name/value pairs.'''
+ return '''\
+// This file is automatically generated by GRIT. Do not edit.
+
+#include <stddef.h>
+
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+ const char* const name;
+ int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+
+extern const GritResourceMap %(map_name)s[];
+extern const size_t %(map_name)sSize;
+''' % { 'map_name': GetMapName(root) }
+
+
+def _FormatSourceHeader(root, output_dir):
+ '''Create the header of the C++ source file for the resource mapping.'''
+ rc_header_file = None
+ map_header_file = None
+ for output in root.GetOutputFiles():
+ type = output.GetType()
+ if 'rc_header' == type:
+ rc_header_file = util.MakeRelativePath(output_dir,
+ output.GetOutputFilename())
+ elif 'resource_map_header' == type:
+ map_header_file = util.MakeRelativePath(output_dir,
+ output.GetOutputFilename())
+ if not rc_header_file or not map_header_file:
+ raise Exception('resource_map_source output type requires '
+ 'a resource_map_header and rc_header outputs')
+ return '''\
+// This file is automatically generated by GRIT. Do not edit.
+
+#include "%(map_header_file)s"
+
+#include <stddef.h>
+
+#include "base/stl_util.h"
+
+#include "%(rc_header_file)s"
+
+const GritResourceMap %(map_name)s[] = {
+''' % { 'map_header_file': map_header_file,
+ 'rc_header_file': rc_header_file,
+ 'map_name': GetMapName(root),
+ }
+
+
+def _FormatSourceFooter(root):
+ # Return the footer text.
+ return '''\
+};
+
+const size_t %(map_name)sSize = base::size(%(map_name)s);
+''' % { 'map_name': GetMapName(root) }
+
+
+def _FormatSource(get_key, root, lang, output_dir):
+ from grit.node import include, structure, message
+ id_map = root.GetIdMap()
+ yield _FormatSourceHeader(root, output_dir)
+ seen = set()
+ for item in root.ActiveDescendants():
+ if not item.IsResourceMapSource():
+ continue
+ key = get_key(item)
+ tid = item.attrs['name']
+ if tid not in id_map or key in seen:
+ continue
+ seen.add(key)
+ yield ' {"%s", %s},\n' % (key, tid)
+ yield _FormatSourceFooter(root)
+
+
+def _GetItemName(item):
+ return item.attrs['name']
+
+# Check if |path2| is a subpath of |path1|.
+def _IsSubpath(path1, path2):
+ path1_abs = os.path.abspath(path1)
+ path2_abs = os.path.abspath(path2)
+ common = os.path.commonprefix([path1_abs, path2_abs])
+ return path1_abs == common
+
+def _GetItemPath(item):
+ path = item.GetInputPath().replace("\\", "/")
+
+ # Handle the case where the file resides within the output folder,
+ # by expanding any variables as well as replacing the output folder name with
+ # a fixed string such that the key added to the map does not depend on a given
+ # developer's setup.
+ #
+ # For example this will convert the following path:
+ # ../../out/gchrome/${root_gen_dir}/ui/webui/resources/js/foo.js
+ # to:
+ # @out_folder@/gen/ui/webui/resources/js/foo.js
+
+ real_path = item.ToRealPath(item.GetInputPath())
+ if (item.attrs.get('use_base_dir', 'true') != 'true' and
+ _IsSubpath(os.path.curdir, real_path)):
+ path = os.path.join(
+ '@out_folder@', os.path.relpath(real_path)).replace("\\", "/")
+
+ assert '$' not in path, 'all variables should have been expanded'
+ return path
diff --git a/tools/grit/grit/format/resource_map_unittest.py b/tools/grit/grit/format/resource_map_unittest.py
new file mode 100644
index 0000000000..3499b321ef
--- /dev/null
+++ b/tools/grit/grit/format/resource_map_unittest.py
@@ -0,0 +1,345 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.resource_map'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit import util
+from grit.format import resource_map
+
+
+class FormatResourceMapUnittest(unittest.TestCase):
+ def testFormatResourceMap(self):
+ grd = util.ParseGrdForUnittest('''
+ <outputs>
+ <output type="rc_header" filename="the_rc_header.h" />
+ <output type="resource_map_header"
+ filename="the_resource_map_header.h" />
+ </outputs>
+ <release seq="3">
+ <structures first_id="300">
+ <structure type="menu" name="IDC_KLONKMENU"
+ file="grit\\testdata\\klonk.rc" encoding="utf-16" />
+ </structures>
+ <includes first_id="10000">
+ <include type="foo" file="abc" name="IDS_FIRSTPRESENT" />
+ <if expr="False">
+ <include type="foo" file="def" name="IDS_MISSING" />
+ </if>
+ <if expr="lang != 'es'">
+ <include type="foo" file="ghi" name="IDS_LANGUAGESPECIFIC" />
+ </if>
+ <if expr="lang == 'es'">
+ <include type="foo" file="jkl" name="IDS_LANGUAGESPECIFIC" />
+ </if>
+ <include type="foo" file="mno" name="IDS_THIRDPRESENT" />
+ <include type="foo" file="opq" name="IDS_FOURTHPRESENT"
+ skip_in_resource_map="true" />
+ </includes>
+ </release>''', run_gatherers=True)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+ const char* const name;
+ int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"IDC_KLONKMENU", IDC_KLONKMENU},
+ {"IDS_FIRSTPRESENT", IDS_FIRSTPRESENT},
+ {"IDS_LANGUAGESPECIFIC", IDS_LANGUAGESPECIFIC},
+ {"IDS_THIRDPRESENT", IDS_THIRDPRESENT},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_file_map_source')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"grit/testdata/klonk.rc", IDC_KLONKMENU},
+ {"abc", IDS_FIRSTPRESENT},
+ {"ghi", IDS_LANGUAGESPECIFIC},
+ {"mno", IDS_THIRDPRESENT},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+ def testFormatResourceMapWithGeneratedFile(self):
+ os.environ["root_gen_dir"] = "gen"
+
+ grd = util.ParseGrdForUnittest('''\
+ <outputs>
+ <output type="rc_header" filename="the_rc_header.h" />
+ <output type="resource_map_header"
+ filename="resource_map_header.h" />
+ </outputs>
+ <release seq="3">
+ <includes first_id="10000">
+ <include type="BINDATA"
+ file="${root_gen_dir}/foo/bar/baz.js"
+ name="IDR_FOO_BAR_BAZ_JS"
+ use_base_dir="false"
+ compress="gzip" />
+ </includes>
+ </release>''', run_gatherers=True)
+
+ formatter = resource_map.GetFormatter('resource_file_map_source')
+ output = util.StripBlankLinesAndComments(''.join(formatter(grd, 'en', '.')))
+ expected = '''\
+#include "resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"@out_folder@/gen/foo/bar/baz.js", IDR_FOO_BAR_BAZ_JS},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);'''
+ self.assertEqual(expected, output)
+
+ def testFormatResourceMapWithOutputAllEqualsFalseForStructures(self):
+ grd = util.ParseGrdForUnittest('''
+ <outputs>
+ <output type="rc_header" filename="the_rc_header.h" />
+ <output type="resource_map_header"
+ filename="the_resource_map_header.h" />
+ <output type="resource_map_source"
+ filename="the_resource_map_header.cc" />
+ </outputs>
+ <release seq="3">
+ <structures first_id="300">
+ <structure type="chrome_scaled_image" name="IDR_KLONKMENU"
+ file="foo.png" />
+ <if expr="False">
+ <structure type="chrome_scaled_image" name="IDR_MISSING"
+ file="bar.png" />
+ </if>
+ <if expr="True">
+ <structure type="chrome_scaled_image" name="IDR_BLOB"
+ file="blob.png" />
+ </if>
+ <if expr="True">
+ <then>
+ <structure type="chrome_scaled_image" name="IDR_METEOR"
+ file="meteor.png" />
+ </then>
+ <else>
+ <structure type="chrome_scaled_image" name="IDR_METEOR"
+ file="roetem.png" />
+ </else>
+ </if>
+ <if expr="False">
+ <structure type="chrome_scaled_image" name="IDR_LAST"
+ file="zyx.png" />
+ </if>
+ <if expr="True">
+ <structure type="chrome_scaled_image" name="IDR_LAST"
+ file="xyz.png" />
+ </if>
+ </structures>
+ </release>''', run_gatherers=True)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+ const char* const name;
+ int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"IDR_KLONKMENU", IDR_KLONKMENU},
+ {"IDR_BLOB", IDR_BLOB},
+ {"IDR_METEOR", IDR_METEOR},
+ {"IDR_LAST", IDR_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"IDR_KLONKMENU", IDR_KLONKMENU},
+ {"IDR_BLOB", IDR_BLOB},
+ {"IDR_METEOR", IDR_METEOR},
+ {"IDR_LAST", IDR_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+ def testFormatResourceMapWithOutputAllEqualsFalseForIncludes(self):
+ grd = util.ParseGrdForUnittest('''
+ <outputs>
+ <output type="rc_header" filename="the_rc_header.h" />
+ <output type="resource_map_header"
+ filename="the_resource_map_header.h" />
+ </outputs>
+ <release seq="3">
+ <structures first_id="300">
+ <structure type="menu" name="IDC_KLONKMENU"
+ file="grit\\testdata\\klonk.rc" encoding="utf-16" />
+ </structures>
+ <includes first_id="10000">
+ <include type="foo" file="abc" name="IDS_FIRSTPRESENT" />
+ <if expr="False">
+ <include type="foo" file="def" name="IDS_MISSING" />
+ </if>
+ <include type="foo" file="mno" name="IDS_THIRDPRESENT" />
+ <if expr="True">
+ <include type="foo" file="blob" name="IDS_BLOB" />
+ </if>
+ <if expr="True">
+ <then>
+ <include type="foo" file="meteor" name="IDS_METEOR" />
+ </then>
+ <else>
+ <include type="foo" file="roetem" name="IDS_METEOR" />
+ </else>
+ </if>
+ <if expr="False">
+ <include type="foo" file="zyx" name="IDS_LAST" />
+ </if>
+ <if expr="True">
+ <include type="foo" file="xyz" name="IDS_LAST" />
+ </if>
+ </includes>
+ </release>''', run_gatherers=True)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+ const char* const name;
+ int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"IDC_KLONKMENU", IDC_KLONKMENU},
+ {"IDS_FIRSTPRESENT", IDS_FIRSTPRESENT},
+ {"IDS_THIRDPRESENT", IDS_THIRDPRESENT},
+ {"IDS_BLOB", IDS_BLOB},
+ {"IDS_METEOR", IDS_METEOR},
+ {"IDS_LAST", IDS_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_file_map_source')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"grit/testdata/klonk.rc", IDC_KLONKMENU},
+ {"abc", IDS_FIRSTPRESENT},
+ {"mno", IDS_THIRDPRESENT},
+ {"blob", IDS_BLOB},
+ {"meteor", IDS_METEOR},
+ {"xyz", IDS_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+ def testFormatStringResourceMap(self):
+ grd = util.ParseGrdForUnittest('''
+ <outputs>
+ <output type="rc_header" filename="the_rc_header.h" />
+ <output type="resource_map_header" filename="the_rc_map_header.h" />
+ <output type="resource_map_source" filename="the_rc_map_source.cc" />
+ </outputs>
+ <release seq="1" allow_pseudo="false">
+ <messages fallback_to_english="true">
+ <message name="IDS_PRODUCT_NAME" desc="The application name">
+ Application
+ </message>
+ <if expr="True">
+ <message name="IDS_DEFAULT_TAB_TITLE_TITLE_CASE"
+ desc="In Title Case: The default title in a tab.">
+ New Tab
+ </message>
+ </if>
+ <if expr="False">
+ <message name="IDS_DEFAULT_TAB_TITLE"
+ desc="The default title in a tab.">
+ New tab
+ </message>
+ </if>
+ </messages>
+ </release>''', run_gatherers=True)
+ grd.InitializeIds()
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+ const char* const name;
+ int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+ output = util.StripBlankLinesAndComments(''.join(
+ resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+ self.assertEqual('''\
+#include "the_rc_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+ {"IDS_PRODUCT_NAME", IDS_PRODUCT_NAME},
+ {"IDS_DEFAULT_TAB_TITLE_TITLE_CASE", IDS_DEFAULT_TAB_TITLE_TITLE_CASE},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/gather/__init__.py b/tools/grit/grit/gather/__init__.py
new file mode 100644
index 0000000000..2d578f5643
--- /dev/null
+++ b/tools/grit/grit/gather/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Module grit.gather
+'''
+
+pass
diff --git a/tools/grit/grit/gather/admin_template.py b/tools/grit/grit/gather/admin_template.py
new file mode 100644
index 0000000000..c26b6a88d7
--- /dev/null
+++ b/tools/grit/grit/gather/admin_template.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Gatherer for administrative template files.
+'''
+
+from __future__ import print_function
+
+import re
+
+from grit.gather import regexp
+from grit import exception
+from grit import lazy_re
+
+
+class MalformedAdminTemplateException(exception.Base):
+ '''This file doesn't look like a .adm file to me.'''
+ pass
+
+
+class AdmGatherer(regexp.RegexpGatherer):
+ '''Gatherer for the translateable portions of an admin template.
+
+ This gatherer currently makes the following assumptions:
+ - there is only one [strings] section and it is always the last section
+ of the file
+ - translateable strings do not need to be escaped.
+ '''
+
+ # Finds the strings section as the group named 'strings'
+ _STRINGS_SECTION = lazy_re.compile(
+ r'(?P<first_part>.+^\[strings\])(?P<strings>.+)\Z',
+ re.MULTILINE | re.DOTALL)
+
+ # Finds the translateable sections from within the [strings] section.
+ _TRANSLATEABLES = lazy_re.compile(
+ r'^\s*[A-Za-z0-9_]+\s*=\s*"(?P<text>.+)"\s*$',
+ re.MULTILINE)
+
+ def Escape(self, text):
+ return text.replace('\n', '\\n')
+
+ def UnEscape(self, text):
+ return text.replace('\\n', '\n')
+
+ def Parse(self):
+ if self.have_parsed_:
+ return
+ self.have_parsed_ = True
+
+ self.text_ = self._LoadInputFile().strip()
+ m = self._STRINGS_SECTION.match(self.text_)
+ if not m:
+ raise MalformedAdminTemplateException()
+ # Add the first part, which is all nontranslateable, to the skeleton
+ self._AddNontranslateableChunk(m.group('first_part'))
+ # Then parse the rest using the _TRANSLATEABLES regexp.
+ self._RegExpParse(self._TRANSLATEABLES, m.group('strings'))
+
+ def GetTextualIds(self):
+ return [self.extkey]
diff --git a/tools/grit/grit/gather/admin_template_unittest.py b/tools/grit/grit/gather/admin_template_unittest.py
new file mode 100644
index 0000000000..c637af3a75
--- /dev/null
+++ b/tools/grit/grit/gather/admin_template_unittest.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for the admin template gatherer.'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import admin_template
+from grit import util
+from grit import grd_reader
+from grit import grit_runner
+from grit.tool import build
+
+
+class AdmGathererUnittest(unittest.TestCase):
+ def testParsingAndTranslating(self):
+ pseudofile = StringIO(
+ 'bingo bongo\n'
+ 'ding dong\n'
+ '[strings] \n'
+ 'whatcha="bingo bongo"\n'
+ 'gotcha = "bingolabongola "the wise" fingulafongula" \n')
+ gatherer = admin_template.AdmGatherer(pseudofile)
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 2)
+ self.failUnless(gatherer.GetCliques()[1].GetMessage().GetRealContent() ==
+ 'bingolabongola "the wise" fingulafongula')
+
+ translation = gatherer.Translate('en')
+ self.failUnless(translation == gatherer.GetText().strip())
+
+ def testErrorHandling(self):
+ pseudofile = StringIO(
+ 'bingo bongo\n'
+ 'ding dong\n'
+ 'whatcha="bingo bongo"\n'
+ 'gotcha = "bingolabongola "the wise" fingulafongula" \n')
+ gatherer = admin_template.AdmGatherer(pseudofile)
+ self.assertRaises(admin_template.MalformedAdminTemplateException,
+ gatherer.Parse)
+
+ _TRANSLATABLES_FROM_FILE = (
+ 'Google', 'Google Desktop', 'Preferences',
+ 'Controls Google Desktop preferences',
+ 'Indexing and Capture Control',
+ 'Controls what files, web pages, and other content will be indexed by Google Desktop.',
+ 'Prevent indexing of email',
+ # there are lots more but we don't check any further
+ )
+
+ def VerifyCliquesFromAdmFile(self, cliques):
+ self.failUnless(len(cliques) > 20)
+ for clique, expected in zip(cliques, self._TRANSLATABLES_FROM_FILE):
+ text = clique.GetMessage().GetRealContent()
+ self.failUnless(text == expected)
+
+ def testFromFile(self):
+ fname = util.PathFromRoot('grit/testdata/GoogleDesktop.adm')
+ gatherer = admin_template.AdmGatherer(fname)
+ gatherer.Parse()
+ cliques = gatherer.GetCliques()
+ self.VerifyCliquesFromAdmFile(cliques)
+
+ def MakeGrd(self):
+ grd = grd_reader.Parse(StringIO('''<?xml version="1.0" encoding="UTF-8"?>
+ <grit latest_public_release="2" source_lang_id="en-US" current_release="3">
+ <release seq="3">
+ <structures>
+ <structure type="admin_template" name="IDAT_GOOGLE_DESKTOP_SEARCH"
+ file="GoogleDesktop.adm" exclude_from_rc="true" />
+ <structure type="txt" name="BINGOBONGO"
+ file="README.txt" exclude_from_rc="true" />
+ </structures>
+ </release>
+ <outputs>
+ <output filename="de_res.rc" type="rc_all" lang="de" />
+ </outputs>
+ </grit>'''), util.PathFromRoot('grit/testdata'))
+ grd.SetOutputLanguage('en')
+ grd.RunGatherers()
+ return grd
+
+ def testInGrd(self):
+ grd = self.MakeGrd()
+ cliques = grd.children[0].children[0].children[0].GetCliques()
+ self.VerifyCliquesFromAdmFile(cliques)
+
+ def testFileIsOutput(self):
+ grd = self.MakeGrd()
+ dirname = util.TempDir({})
+ try:
+ tool = build.RcBuilder()
+ tool.o = grit_runner.Options()
+ tool.output_directory = dirname.GetPath()
+ tool.res = grd
+ tool.Process()
+
+ self.failUnless(os.path.isfile(dirname.GetPath('de_GoogleDesktop.adm')))
+ self.failUnless(os.path.isfile(dirname.GetPath('de_README.txt')))
+ finally:
+ dirname.CleanUp()
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/gather/chrome_html.py b/tools/grit/grit/gather/chrome_html.py
new file mode 100644
index 0000000000..71c1332d66
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_html.py
@@ -0,0 +1,377 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Prepares a Chrome HTML file by inlining resources and adding references to
+high DPI resources and removing references to unsupported scale factors.
+
+This is a small gatherer that takes a HTML file, looks for src attributes
+and inlines the specified file, producing one HTML file with no external
+dependencies. It recursively inlines the included files. When inlining CSS
+image files this script also checks for the existence of high DPI versions
+of the inlined file including those on relevant platforms. Unsupported scale
+factors are also removed from existing image sets to support explicitly
+referencing all available images.
+"""
+
+from __future__ import print_function
+
+import os
+import re
+
+from grit import lazy_re
+from grit import util
+from grit.format import html_inline
+from grit.gather import interface
+
+
+# Distribution string to replace with distribution.
+DIST_SUBSTR = '%DISTRIBUTION%'
+
+
+# Matches a chrome theme source URL.
+_THEME_SOURCE = lazy_re.compile(
+ r'(?P<baseurl>chrome://theme/IDR_[A-Z0-9_]*)(?P<query>\?.*)?')
+# Pattern for matching CSS url() function.
+_CSS_URL_PATTERN = r'url\((?P<quote>"|\'|)(?P<filename>[^"\'()]*)(?P=quote)\)'
+# Matches CSS url() functions with the capture group 'filename'.
+_CSS_URL = lazy_re.compile(_CSS_URL_PATTERN)
+# Matches one or more CSS image urls used in given properties.
+_CSS_IMAGE_URLS = lazy_re.compile(
+ r'(?P<attribute>content|background|[\w-]*-image):\s*'
+ r'(?P<urls>(' + _CSS_URL_PATTERN + r'\s*,?\s*)+)')
+# Matches CSS image sets.
+_CSS_IMAGE_SETS = lazy_re.compile(
+ r'(?P<attribute>content|background|[\w-]*-image):[ ]*'
+ r'-webkit-image-set\((?P<images>'
+ r'(\s*,?\s*url\((?P<quote>"|\'|)[^"\'()]*(?P=quote)\)[ ]*[0-9.]*x)*)\)',
+ re.MULTILINE)
+# Matches a single image in a CSS image set with the capture group scale.
+_CSS_IMAGE_SET_IMAGE = lazy_re.compile(r'\s*,?\s*'
+ r'url\((?P<quote>"|\'|)[^"\'()]*(?P=quote)\)[ ]*(?P<scale>[0-9.]*x)',
+ re.MULTILINE)
+_HTML_IMAGE_SRC = lazy_re.compile(
+ r'<img[^>]+src=\"(?P<filename>[^">]*)\"[^>]*>')
+
+def GetImageList(
+ base_path, filename, scale_factors, distribution,
+ filename_expansion_function=None):
+ """Generate the list of images which match the provided scale factors.
+
+ Takes an image filename and checks for files of the same name in folders
+ corresponding to the supported scale factors. If the file is from a
+ chrome://theme/ source, inserts supported @Nx scale factors as high DPI
+ versions.
+
+ Args:
+ base_path: path to look for relative file paths in
+ filename: name of the base image file
+ scale_factors: a list of the supported scale factors (i.e. ['2x'])
+ distribution: string that should replace %DISTRIBUTION%
+
+ Returns:
+ array of tuples containing scale factor and image (i.e.
+ [('1x', 'image.png'), ('2x', '2x/image.png')]).
+ """
+ # Any matches for which a chrome URL handler will serve all scale factors
+ # can simply request all scale factors.
+ theme_match = _THEME_SOURCE.match(filename)
+ if theme_match:
+ images = [('1x', filename)]
+ for scale_factor in scale_factors:
+ scale_filename = "%s@%s" % (theme_match.group('baseurl'), scale_factor)
+ if theme_match.group('query'):
+ scale_filename += theme_match.group('query')
+ images.append((scale_factor, scale_filename))
+ return images
+
+ if filename.find(':') != -1:
+ # filename is probably a URL, only return filename itself.
+ return [('1x', filename)]
+
+ filename = filename.replace(DIST_SUBSTR, distribution)
+ if filename_expansion_function:
+ filename = filename_expansion_function(filename)
+ filepath = os.path.join(base_path, filename)
+ images = [('1x', filename)]
+
+ for scale_factor in scale_factors:
+ # Check for existence of file and add to image set.
+ scale_path = os.path.split(os.path.join(base_path, filename))
+ scale_image_path = os.path.join(scale_path[0], scale_factor, scale_path[1])
+ if os.path.isfile(scale_image_path):
+ # HTML/CSS always uses forward slashed paths.
+ parts = filename.rsplit('/', 1)
+ if len(parts) == 1:
+ path = ''
+ else:
+ path = parts[0] + '/'
+ scale_image_name = path + scale_factor + '/' + parts[-1]
+ images.append((scale_factor, scale_image_name))
+ return images
+
+
+def GenerateImageSet(images, quote):
+ """Generates a -webkit-image-set for the provided list of images.
+
+ Args:
+ images: an array of tuples giving scale factor and file path
+ (i.e. [('1x', 'image.png'), ('2x', '2x/image.png')]).
+ quote: a string giving the quotation character to use (i.e. "'")
+
+ Returns:
+ string giving a -webkit-image-set rule referencing the provided images.
+ (i.e. '-webkit-image-set(url('image.png') 1x, url('2x/image.png') 2x)')
+ """
+ imageset = []
+ for (scale_factor, filename) in images:
+ imageset.append("url(%s%s%s) %s" % (quote, filename, quote, scale_factor))
+ return "-webkit-image-set(%s)" % (', '.join(imageset))
+
+
+def UrlToImageSet(
+ src_match, base_path, scale_factors, distribution,
+ filename_expansion_function=None):
+ """Regex replace function which replaces url() with -webkit-image-set.
+
+ Takes a regex match for url('path'). If the file is local, checks for
+ files of the same name in folders corresponding to the supported scale
+ factors. If the file is from a chrome://theme/ source, inserts the
+ supported @Nx scale factor request. In either case inserts a
+ -webkit-image-set rule to fetch the appropriate image for the current
+ scale factor.
+
+ Args:
+ src_match: regex match object from _CSS_URLS
+ base_path: path to look for relative file paths in
+ scale_factors: a list of the supported scale factors (i.e. ['2x'])
+ distribution: string that should replace %DISTRIBUTION%.
+
+ Returns:
+ string
+ """
+ quote = src_match.group('quote')
+ filename = src_match.group('filename')
+ image_list = GetImageList(
+ base_path, filename, scale_factors, distribution,
+ filename_expansion_function=filename_expansion_function)
+
+ # Don't modify the source if there is only one image.
+ if len(image_list) == 1:
+ return src_match.group(0)
+
+ return GenerateImageSet(image_list, quote)
+
+
+def InsertImageSet(
+ src_match, base_path, scale_factors, distribution,
+ filename_expansion_function=None):
+ """Regex replace function which inserts -webkit-image-set rules.
+
+ Takes a regex match for `property: url('path')[, url('path')]+`.
+ Replaces one or more occurances of the match with image set rules.
+
+ Args:
+ src_match: regex match object from _CSS_IMAGE_URLS
+ base_path: path to look for relative file paths in
+ scale_factors: a list of the supported scale factors (i.e. ['2x'])
+ distribution: string that should replace %DISTRIBUTION%.
+
+ Returns:
+ string
+ """
+ attr = src_match.group('attribute')
+ urls = _CSS_URL.sub(
+ lambda m: UrlToImageSet(m, base_path, scale_factors, distribution,
+ filename_expansion_function),
+ src_match.group('urls'))
+
+ return "%s: %s" % (attr, urls)
+
+
+def InsertImageStyle(
+ src_match, base_path, scale_factors, distribution,
+ filename_expansion_function=None):
+ """Regex replace function which adds a content style to an <img>.
+
+ Takes a regex match from _HTML_IMAGE_SRC and replaces the attribute with a CSS
+ style which defines the image set.
+ """
+ filename = src_match.group('filename')
+ image_list = GetImageList(
+ base_path, filename, scale_factors, distribution,
+ filename_expansion_function=filename_expansion_function)
+
+ # Don't modify the source if there is only one image or image already defines
+ # a style.
+ if src_match.group(0).find(" style=\"") != -1 or len(image_list) == 1:
+ return src_match.group(0)
+
+ return "%s style=\"content: %s;\">" % (src_match.group(0)[:-1],
+ GenerateImageSet(image_list, "'"))
+
+
+def InsertImageSets(
+ filepath, text, scale_factors, distribution,
+ filename_expansion_function=None):
+ """Helper function that adds references to external images available in any of
+ scale_factors in CSS backgrounds.
+ """
+ # Add high DPI urls for css attributes: content, background,
+ # or *-image or <img src="foo">.
+ return _CSS_IMAGE_URLS.sub(
+ lambda m: InsertImageSet(
+ m, filepath, scale_factors, distribution,
+ filename_expansion_function=filename_expansion_function),
+ _HTML_IMAGE_SRC.sub(
+ lambda m: InsertImageStyle(
+ m, filepath, scale_factors, distribution,
+ filename_expansion_function=filename_expansion_function),
+ text))
+
+
+def RemoveImagesNotIn(scale_factors, src_match):
+ """Regex replace function which removes images for scale factors not in
+ scale_factors.
+
+ Takes a regex match for _CSS_IMAGE_SETS. For each image in the group images,
+ checks if this scale factor is in scale_factors and if not, removes it.
+
+ Args:
+ scale_factors: a list of the supported scale factors (i.e. ['1x', '2x'])
+ src_match: regex match object from _CSS_IMAGE_SETS
+
+ Returns:
+ string
+ """
+ attr = src_match.group('attribute')
+ images = _CSS_IMAGE_SET_IMAGE.sub(
+ lambda m: m.group(0) if m.group('scale') in scale_factors else '',
+ src_match.group('images'))
+ return "%s: -webkit-image-set(%s)" % (attr, images)
+
+
+def RemoveImageSetImages(text, scale_factors):
+ """Helper function which removes images in image sets not in the list of
+ supported scale_factors.
+ """
+ return _CSS_IMAGE_SETS.sub(
+ lambda m: RemoveImagesNotIn(scale_factors, m), text)
+
+
+def ProcessImageSets(
+ filepath, text, scale_factors, distribution,
+ filename_expansion_function=None):
+ """Helper function that adds references to external images available in other
+ scale_factors and removes images from image-sets in unsupported scale_factors.
+ """
+ # Explicitly add 1x to supported scale factors so that it is not removed.
+ supported_scale_factors = ['1x']
+ supported_scale_factors.extend(scale_factors)
+ return InsertImageSets(
+ filepath,
+ RemoveImageSetImages(text, supported_scale_factors),
+ scale_factors,
+ distribution,
+ filename_expansion_function=filename_expansion_function)
+
+
+class ChromeHtml(interface.GathererBase):
+ """Represents an HTML document processed for Chrome WebUI.
+
+ HTML documents used in Chrome WebUI have local resources inlined and
+ automatically insert references to high DPI assets used in CSS properties
+ with the use of the -webkit-image-set value. References to unsupported scale
+ factors in image sets are also removed. This does not generate any
+ translateable messages and instead generates a single DataPack resource.
+ """
+
+ def __init__(self, *args, **kwargs):
+ super(ChromeHtml, self).__init__(*args, **kwargs)
+ self.allow_external_script_ = False
+ self.flatten_html_ = False
+ self.preprocess_only_ = False
+ # 1x resources are implicitly already in the source and do not need to be
+ # added.
+ self.scale_factors_ = []
+ self.filename_expansion_function = None
+
+ def SetAttributes(self, attrs):
+ self.allow_external_script_ = ('allowexternalscript' in attrs and
+ attrs['allowexternalscript'] == 'true')
+ self.preprocess_only_ = ('preprocess' in attrs and
+ attrs['preprocess'] == 'true')
+ self.flatten_html_ = (self.preprocess_only_ or ('flattenhtml' in attrs and
+ attrs['flattenhtml'] == 'true'))
+
+ def SetDefines(self, defines):
+ if 'scale_factors' in defines:
+ self.scale_factors_ = defines['scale_factors'].split(',')
+
+ def GetText(self):
+ """Returns inlined text of the HTML document."""
+ return self.inlined_text_
+
+ def GetTextualIds(self):
+ return [self.extkey]
+
+ def GetData(self, lang, encoding):
+ """Returns inlined text of the HTML document."""
+ ret = self.inlined_text_
+ if encoding == util.BINARY:
+ ret = ret.encode('utf-8')
+ return ret
+
+ def GetHtmlResourceFilenames(self):
+ """Returns a set of all filenames inlined by this file."""
+ if self.flatten_html_:
+ return html_inline.GetResourceFilenames(
+ self.grd_node.ToRealPath(self.GetInputPath()),
+ self.grd_node,
+ allow_external_script=self.allow_external_script_,
+ rewrite_function=lambda fp, t, d: ProcessImageSets(
+ fp, t, self.scale_factors_, d,
+ filename_expansion_function=self.filename_expansion_function),
+ filename_expansion_function=self.filename_expansion_function)
+ return []
+
+ def Translate(self, lang, pseudo_if_not_available=True,
+ skeleton_gatherer=None, fallback_to_english=False):
+ """Returns this document translated."""
+ return self.inlined_text_
+
+ def SetFilenameExpansionFunction(self, fn):
+ self.filename_expansion_function = fn
+
+ def Parse(self):
+ """Parses and inlines the represented file."""
+
+ filename = self.GetInputPath()
+ # If there is a grd_node, prefer its GetInputPath(), as that may do more
+ # processing to make the call to ToRealPath() below work correctly.
+ if self.grd_node:
+ filename = self.grd_node.GetInputPath()
+ if self.filename_expansion_function:
+ filename = self.filename_expansion_function(filename)
+ # Hack: some unit tests supply an absolute path and no root node.
+ if not os.path.isabs(filename):
+ filename = self.grd_node.ToRealPath(filename)
+ if self.flatten_html_:
+ self.inlined_text_ = html_inline.InlineToString(
+ filename,
+ self.grd_node,
+ allow_external_script = self.allow_external_script_,
+ strip_whitespace=True,
+ preprocess_only = self.preprocess_only_,
+ rewrite_function=lambda fp, t, d: ProcessImageSets(
+ fp, t, self.scale_factors_, d,
+ filename_expansion_function=self.filename_expansion_function),
+ filename_expansion_function=self.filename_expansion_function)
+ else:
+ distribution = html_inline.GetDistribution()
+ self.inlined_text_ = ProcessImageSets(
+ os.path.dirname(filename),
+ util.ReadFile(filename, 'utf-8'),
+ self.scale_factors_,
+ distribution,
+ filename_expansion_function=self.filename_expansion_function)
diff --git a/tools/grit/grit/gather/chrome_html_unittest.py b/tools/grit/grit/gather/chrome_html_unittest.py
new file mode 100644
index 0000000000..8c75ee5bf4
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_html_unittest.py
@@ -0,0 +1,610 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.chrome_html'''
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit import lazy_re
+from grit import util
+from grit.gather import chrome_html
+
+
+_NEW_LINE = lazy_re.compile('(\r\n|\r|\n)', re.MULTILINE)
+
+
+def StandardizeHtml(text):
+ '''Standardizes the newline format and png mime type in Html text.'''
+ return _NEW_LINE.sub('\n', text).replace('data:image/x-png;',
+ 'data:image/png;')
+
+
+class ChromeHtmlUnittest(unittest.TestCase):
+ '''Unit tests for ChromeHtml.'''
+
+ def testFileResources(self):
+ '''Tests inlined image file resources with available high DPI assets.'''
+
+ tmp_dir = util.TempDir({
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ ''',
+
+ 'test.css': '''
+ .image {
+ background: url('test.png');
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '1.4x/test.png': '1.4x PNG DATA',
+
+ '1.8x/test.png': '1.8x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+ html.SetDefines({'scale_factors': '1.4x,1.8x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <style>
+ .image {
+ background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MS40eCBQTkcgREFUQQ==') 1.4x, url('data:image/png;base64,MS44eCBQTkcgREFUQQ==') 1.8x);
+ }
+ </style>
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesImageTag(self):
+ '''Tests inlined image file resources with available high DPI assets on
+ an image tag.'''
+
+ tmp_dir = util.TempDir({
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <body>
+ <img id="foo" src="test.png">
+ </body>
+ </html>
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ <!DOCTYPE HTML>
+ <html>
+ <body>
+ <img id="foo" src="data:image/png;base64,UE5HIERBVEE=" style="content: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MnggUE5HIERBVEE=') 2x);">
+ </body>
+ </html>
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesNoFlatten(self):
+ '''Tests non-inlined image file resources with available high DPI assets.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url('test.png');
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '1.4x/test.png': '1.4x PNG DATA',
+
+ '1.8x/test.png': '1.8x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '1.4x,1.8x'})
+ html.SetAttributes({'flattenhtml': 'false'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url('test.png') 1x, url('1.4x/test.png') 1.4x, url('1.8x/test.png') 1.8x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesNoFlattenSubdir(self):
+ '''Tests non-inlined image file resources w/high DPI assets in subdirs.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url('sub/test.png');
+ }
+ ''',
+
+ 'sub/test.png': 'PNG DATA',
+
+ 'sub/1.4x/test.png': '1.4x PNG DATA',
+
+ 'sub/1.8x/test.png': '1.8x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '1.4x,1.8x'})
+ html.SetAttributes({'flattenhtml': 'false'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url('sub/test.png') 1x, url('sub/1.4x/test.png') 1.4x, url('sub/1.8x/test.png') 1.8x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesPreprocess(self):
+ '''Tests preprocessed image file resources with available high DPI
+ assets.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url('test.png');
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '1.4x/test.png': '1.4x PNG DATA',
+
+ '1.8x/test.png': '1.8x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '1.4x,1.8x'})
+ html.SetAttributes({'flattenhtml': 'false', 'preprocess': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url('test.png') 1x, url('1.4x/test.png') 1.4x, url('1.8x/test.png') 1.8x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesDoubleQuotes(self):
+ '''Tests inlined image file resources if url() filename is double quoted.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url("test.png");
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url("data:image/png;base64,UE5HIERBVEE=") 1x, url("data:image/png;base64,MnggUE5HIERBVEE=") 2x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesNoQuotes(self):
+ '''Tests inlined image file resources when url() filename is unquoted.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url(test.png);
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesSubdirs(self):
+ '''Tests inlined image file resources if url() filename is in a subdir.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url('some/sub/path/test.png');
+ }
+ ''',
+
+ 'some/sub/path/test.png': 'PNG DATA',
+
+ 'some/sub/path/2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MnggUE5HIERBVEE=') 2x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesNoFile(self):
+ '''Tests inlined image file resources without available high DPI assets.'''
+
+ tmp_dir = util.TempDir({
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ ''',
+
+ 'test.css': '''
+ .image {
+ background: url('test.png');
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <style>
+ .image {
+ background: url('data:image/png;base64,UE5HIERBVEE=');
+ }
+ </style>
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesMultipleBackgrounds(self):
+ '''Tests inlined image file resources with two url()s.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url(test.png), url(test.png);
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x), -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesMultipleBackgroundsWithNewline1(self):
+ '''Tests inlined image file resources with line break after first url().'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background: url(test.png),
+ url(test.png);
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x),
+ -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesMultipleBackgroundsWithNewline2(self):
+ '''Tests inlined image file resources with line break before first url()
+ and before second url().'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background:
+ url(test.png),
+ url(test.png);
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x),
+ -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testFileResourcesCRLF(self):
+ '''Tests inlined image file resource when url() is preceded by a Windows
+ style line break.'''
+
+ tmp_dir = util.TempDir({
+ 'test.css': '''
+ .image {
+ background:\r\nurl(test.png);
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ '2x/test.png': '2x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ .image {
+ background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+ }
+ '''))
+ tmp_dir.CleanUp()
+
+ def testThemeResources(self):
+ '''Tests inserting high DPI chrome://theme references.'''
+
+ tmp_dir = util.TempDir({
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ ''',
+
+ 'test.css': '''
+ .image {
+ background: url('chrome://theme/IDR_RESOURCE_NAME');
+ content: url('chrome://theme/IDR_RESOURCE_NAME_WITH_Q?$1');
+ }
+ ''',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+ html.SetDefines({'scale_factors': '2x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <style>
+ .image {
+ background: -webkit-image-set(url('chrome://theme/IDR_RESOURCE_NAME') 1x, url('chrome://theme/IDR_RESOURCE_NAME@2x') 2x);
+ content: -webkit-image-set(url('chrome://theme/IDR_RESOURCE_NAME_WITH_Q?$1') 1x, url('chrome://theme/IDR_RESOURCE_NAME_WITH_Q@2x?$1') 2x);
+ }
+ </style>
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ '''))
+ tmp_dir.CleanUp()
+
+ def testRemoveUnsupportedScale(self):
+ '''Tests removing an unsupported scale factor from an explicit image-set.'''
+
+ tmp_dir = util.TempDir({
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ ''',
+
+ 'test.css': '''
+ .image {
+ background: -webkit-image-set(url('test.png') 1x,
+ url('test1.4.png') 1.4x,
+ url('test1.8.png') 1.8x);
+ }
+ ''',
+
+ 'test.png': 'PNG DATA',
+
+ 'test1.4.png': '1.4x PNG DATA',
+
+ 'test1.8.png': '1.8x PNG DATA',
+ })
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+ html.SetDefines({'scale_factors': '1.8x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <style>
+ .image {
+ background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x,
+ url('data:image/png;base64,MS44eCBQTkcgREFUQQ==') 1.8x);
+ }
+ </style>
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ '''))
+ tmp_dir.CleanUp()
+
+ def testExpandVariablesInFilename(self):
+ '''
+ Tests variable substitution in filenames while flattening images
+ with multiple scale factors.
+ '''
+
+ tmp_dir = util.TempDir({
+ 'index.html': '''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <link rel="stylesheet" href="test.css">
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ ''',
+
+ 'test.css': '''
+ .image {
+ background: url('test[WHICH].png');
+ }
+ ''',
+
+ 'test1.png': 'PNG DATA',
+ '1.4x/test1.png': '1.4x PNG DATA',
+ '1.8x/test1.png': '1.8x PNG DATA',
+ })
+
+ def replacer(var, repl):
+ return lambda filename: filename.replace('[%s]' % var, repl)
+
+ html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+ html.SetDefines({'scale_factors': '1.4x,1.8x'})
+ html.SetAttributes({'flattenhtml': 'true'})
+ html.SetFilenameExpansionFunction(replacer('WHICH', '1'));
+ html.Parse()
+ self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+ StandardizeHtml('''
+ <!DOCTYPE HTML>
+ <html>
+ <head>
+ <style>
+ .image {
+ background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MS40eCBQTkcgREFUQQ==') 1.4x, url('data:image/png;base64,MS44eCBQTkcgREFUQQ==') 1.8x);
+ }
+ </style>
+ </head>
+ <body>
+ <!-- Don't need a body. -->
+ </body>
+ </html>
+ '''))
+ tmp_dir.CleanUp()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/gather/chrome_scaled_image.py b/tools/grit/grit/gather/chrome_scaled_image.py
new file mode 100644
index 0000000000..44f98cbcf0
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_scaled_image.py
@@ -0,0 +1,157 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Gatherer for <structure type="chrome_scaled_image">.
+'''
+
+from __future__ import print_function
+
+import os
+import struct
+
+from grit import exception
+from grit import lazy_re
+from grit import util
+from grit.gather import interface
+
+
+_PNG_SCALE_CHUNK = b'\0\0\0\0csCl\xc1\x30\x60\x4d'
+
+
+def _RescaleImage(data, from_scale, to_scale):
+ if from_scale != to_scale:
+ assert from_scale == 100
+ # Rather than rescaling the image we add a custom chunk directing Chrome to
+ # rescale it on load. Just append it to the PNG data since
+ # _MoveSpecialChunksToFront will move it later anyway.
+ data += _PNG_SCALE_CHUNK
+ return data
+
+
+_PNG_MAGIC = b'\x89PNG\r\n\x1a\n'
+
+'''Mandatory first chunk in order for the png to be valid.'''
+_FIRST_CHUNK = b'IHDR'
+
+'''Special chunks to move immediately after the IHDR chunk. (so that the PNG
+remains valid.)
+'''
+_SPECIAL_CHUNKS = frozenset(b'csCl npTc'.split())
+
+'''Any ancillary chunk not in this list is deleted from the PNG.'''
+_ANCILLARY_CHUNKS_TO_LEAVE = frozenset(
+ b'bKGD cHRM gAMA iCCP pHYs sBIT sRGB tRNS acTL fcTL fdAT'.split())
+
+
+def _MoveSpecialChunksToFront(data):
+ '''Move special chunks immediately after the IHDR chunk (so that the PNG
+ remains valid). Also delete ancillary chunks that are not on our whitelist.
+ '''
+ first = [_PNG_MAGIC]
+ special_chunks = []
+ rest = []
+ for chunk in _ChunkifyPNG(data):
+ type = chunk[4:8]
+ critical = type < b'a'
+ if type == _FIRST_CHUNK:
+ first.append(chunk)
+ elif type in _SPECIAL_CHUNKS:
+ special_chunks.append(chunk)
+ elif critical or type in _ANCILLARY_CHUNKS_TO_LEAVE:
+ rest.append(chunk)
+ return b''.join(first + special_chunks + rest)
+
+
+def _ChunkifyPNG(data):
+ '''Given a PNG image, yield its chunks in order.'''
+ assert data.startswith(_PNG_MAGIC)
+ pos = 8
+ while pos != len(data):
+ length = 12 + struct.unpack_from('>I', data, pos)[0]
+ assert 12 <= length <= len(data) - pos
+ yield data[pos:pos+length]
+ pos += length
+
+
+def _MakeBraceGlob(strings):
+ '''Given ['foo', 'bar'], return '{foo,bar}', for error reporting.
+ '''
+ if len(strings) == 1:
+ return strings[0]
+ else:
+ return '{' + ','.join(strings) + '}'
+
+
+class ChromeScaledImage(interface.GathererBase):
+ '''Represents an image that exists in multiple layout variants
+ (e.g. "default", "touch") and multiple scale variants
+ (e.g. "100_percent", "200_percent").
+ '''
+
+ split_context_re_ = lazy_re.compile(r'(.+)_(\d+)_percent\Z')
+
+ def _FindInputFile(self):
+ output_context = self.grd_node.GetRoot().output_context
+ match = self.split_context_re_.match(output_context)
+ if not match:
+ raise exception.MissingMandatoryAttribute(
+ 'All <output> nodes must have an appropriate context attribute'
+ ' (e.g. context="touch_200_percent")')
+ req_layout, req_scale = match.group(1), int(match.group(2))
+
+ layouts = [req_layout]
+ try_default_layout = self.grd_node.GetRoot().fallback_to_default_layout
+ if try_default_layout and 'default' not in layouts:
+ layouts.append('default')
+
+ scales = [req_scale]
+ try_low_res = self.grd_node.FindBooleanAttribute(
+ 'fallback_to_low_resolution', default=False, skip_self=False)
+ if try_low_res and 100 not in scales:
+ scales.append(100)
+
+ for layout in layouts:
+ for scale in scales:
+ dir = '%s_%s_percent' % (layout, scale)
+ path = os.path.join(dir, self.rc_file)
+ if os.path.exists(self.grd_node.ToRealPath(path)):
+ return path, scale, req_scale
+
+ if not try_default_layout:
+ # The file was not found in the specified output context and it was
+ # explicitly indicated that the default context should not be searched
+ # as a fallback, so return an empty path.
+ return None, 100, req_scale
+
+ # The file was found in neither the specified context nor the default
+ # context, so raise an exception.
+ dir = "%s_%s_percent" % (_MakeBraceGlob(layouts),
+ _MakeBraceGlob([str(x) for x in scales]))
+ raise exception.FileNotFound(
+ 'Tried ' + self.grd_node.ToRealPath(os.path.join(dir, self.rc_file)))
+
+ def GetInputPath(self):
+ path, scale, req_scale = self._FindInputFile()
+ return path
+
+ def Parse(self):
+ pass
+
+ def GetTextualIds(self):
+ return [self.extkey]
+
+ def GetData(self, lang, encoding):
+ assert encoding == util.BINARY
+
+ path, scale, req_scale = self._FindInputFile()
+ if path is None:
+ return None
+
+ data = util.ReadFile(self.grd_node.ToRealPath(path), util.BINARY)
+ data = _RescaleImage(data, scale, req_scale)
+ data = _MoveSpecialChunksToFront(data)
+ return data
+
+ def Translate(self, *args, **kwargs):
+ return self.GetData()
diff --git a/tools/grit/grit/gather/chrome_scaled_image_unittest.py b/tools/grit/grit/gather/chrome_scaled_image_unittest.py
new file mode 100644
index 0000000000..1cebfc6de2
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_scaled_image_unittest.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for ChromeScaledImage.'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
+ '../..')))
+
+import re
+import struct
+import unittest
+import zlib
+
+from grit import exception
+from grit import util
+from grit.format import data_pack
+from grit.tool import build
+
+
+_OUTFILETYPES = [
+ ('.h', 'rc_header'),
+ ('_map.cc', 'resource_map_source'),
+ ('_map.h', 'resource_map_header'),
+ ('.pak', 'data_package'),
+ ('.rc', 'rc_all'),
+]
+
+
+_PNG_HEADER = (
+ b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52'
+ b'\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53'
+ b'\xde')
+_PNG_FOOTER = (
+ b'\x00\x00\x00\x0c\x49\x44\x41\x54\x18\x57\x63\xf8\xff\xff\x3f\x00'
+ b'\x05\xfe\x02\xfe\xa7\x35\x81\x84\x00\x00\x00\x00\x49\x45\x4e\x44'
+ b'\xae\x42\x60\x82')
+
+
+def _MakePNG(chunks):
+ # Python 3 changed the return value of zlib.crc32 to an unsigned int.
+ format = 'i' if sys.version_info.major < 3 else 'I'
+ pack_int32 = struct.Struct('>' + format).pack
+ chunks = [pack_int32(len(payload)) + type + payload +
+ pack_int32(zlib.crc32(type + payload))
+ for type, payload in chunks]
+ return _PNG_HEADER + b''.join(chunks) + _PNG_FOOTER
+
+
+def _GetFilesInPak(pakname):
+ '''Get a set of the files that were actually included in the .pak output.
+ '''
+ return set(data_pack.ReadDataPack(pakname).resources.values())
+
+
+def _GetFilesInRc(rcname, tmp_dir, contents):
+ '''Get a set of the files that were actually included in the .rc output.
+ '''
+ data = util.ReadFile(rcname, util.BINARY).decode('utf-16')
+ contents = dict((tmp_dir.GetPath(k), v) for k, v in contents.items())
+ return set(contents[os.path.normpath(m.group(1))]
+ for m in re.finditer(r'(?m)^\w+\s+BINDATA\s+"([^"]+)"$', data))
+
+
+def _MakeFallbackAttr(fallback):
+ if fallback is None:
+ return ''
+ else:
+ return ' fallback_to_low_resolution="%s"' % ('false', 'true')[fallback]
+
+
+def _Structures(fallback, *body):
+ return '<structures%s>\n%s\n</structures>' % (
+ _MakeFallbackAttr(fallback), '\n'.join(body))
+
+
+def _Structure(name, file, fallback=None):
+ return '<structure name="%s" file="%s" type="chrome_scaled_image"%s />' % (
+ name, file, _MakeFallbackAttr(fallback))
+
+
+def _If(expr, *body):
+ return '<if expr="%s">\n%s\n</if>' % (expr, '\n'.join(body))
+
+
+def _RunBuildTest(self, structures, inputs, expected_outputs, skip_rc=False,
+ layout_fallback=''):
+ outputs = '\n'.join('<output filename="out/%s%s" type="%s" context="%s"%s />'
+ % (context, ext, type, context, layout_fallback)
+ for ext, type in _OUTFILETYPES
+ for context in expected_outputs)
+
+ infiles = {
+ 'in/in.grd': ('''<?xml version="1.0" encoding="UTF-8"?>
+ <grit latest_public_release="0" current_release="1">
+ <outputs>
+ %s
+ </outputs>
+ <release seq="1">
+ %s
+ </release>
+ </grit>
+ ''' % (outputs, structures)).encode('utf-8'),
+ }
+ for pngpath, pngdata in inputs.items():
+ normpath = os.path.normpath('in/' + pngpath)
+ infiles[normpath] = pngdata
+ class Options(object):
+ pass
+
+ with util.TempDir(infiles, mode='wb') as tmp_dir:
+ with tmp_dir.AsCurrentDir():
+ options = Options()
+ options.input = tmp_dir.GetPath('in/in.grd')
+ options.verbose = False
+ options.extra_verbose = False
+ build.RcBuilder().Run(options, [])
+ for context, expected_data in expected_outputs.items():
+ self.assertEquals(expected_data,
+ _GetFilesInPak(tmp_dir.GetPath('out/%s.pak' % context)))
+ if not skip_rc:
+ self.assertEquals(expected_data,
+ _GetFilesInRc(tmp_dir.GetPath('out/%s.rc' % context),
+ tmp_dir, infiles))
+
+
+class ChromeScaledImageUnittest(unittest.TestCase):
+ def testNormalFallback(self):
+ d123a = _MakePNG([(b'AbCd', b'')])
+ t123a = _MakePNG([(b'EfGh', b'')])
+ d123b = _MakePNG([(b'IjKl', b'')])
+ _RunBuildTest(self,
+ _Structures(None,
+ _Structure('IDR_A', 'a.png'),
+ _Structure('IDR_B', 'b.png'),
+ ),
+ {'default_123_percent/a.png': d123a,
+ 'tactile_123_percent/a.png': t123a,
+ 'default_123_percent/b.png': d123b,
+ },
+ {'default_123_percent': set([d123a, d123b]),
+ 'tactile_123_percent': set([t123a, d123b]),
+ })
+
+ def testNormalFallbackFailure(self):
+ self.assertRaises(
+ exception.FileNotFound, _RunBuildTest, self,
+ _Structures(
+ None,
+ _Structure('IDR_A', 'a.png'),
+ ), {
+ 'default_100_percent/a.png': _MakePNG([(b'AbCd', b'')]),
+ 'tactile_100_percent/a.png': _MakePNG([(b'EfGh', b'')]),
+ }, {'tactile_123_percent': 'should fail before using this'})
+
+ def testLowresFallback(self):
+ png = _MakePNG([(b'Abcd', b'')])
+ png_with_csCl = _MakePNG([(b'csCl', b''), (b'Abcd', b'')])
+ for outer in (None, False, True):
+ for inner in (None, False, True):
+ args = (
+ self,
+ _Structures(outer,
+ _Structure('IDR_A', 'a.png', inner),
+ ),
+ {'default_100_percent/a.png': png},
+ {'tactile_200_percent': set([png_with_csCl])})
+ if inner or (inner is None and outer):
+ # should fall back to 100%
+ _RunBuildTest(*args, skip_rc=True)
+ else:
+ # shouldn't fall back
+ self.assertRaises(exception.FileNotFound, _RunBuildTest, *args)
+
+ # Test fallback failure with fallback_to_low_resolution=True
+ self.assertRaises(exception.FileNotFound,
+ _RunBuildTest, self,
+ _Structures(True,
+ _Structure('IDR_A', 'a.png'),
+ ),
+ {}, # no files
+ {'tactile_123_percent': 'should fail before using this'})
+
+ def testNoFallbackToDefaultLayout(self):
+ d123a = _MakePNG([(b'AbCd', b'')])
+ t123a = _MakePNG([(b'EfGh', b'')])
+ d123b = _MakePNG([(b'IjKl', b'')])
+ _RunBuildTest(self,
+ _Structures(None,
+ _Structure('IDR_A', 'a.png'),
+ _Structure('IDR_B', 'b.png'),
+ ),
+ {'default_123_percent/a.png': d123a,
+ 'tactile_123_percent/a.png': t123a,
+ 'default_123_percent/b.png': d123b,
+ },
+ {'default_123_percent': set([d123a, d123b]),
+ 'tactile_123_percent': set([t123a]),
+ },
+ layout_fallback=' fallback_to_default_layout="false"')
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/gather/interface.py b/tools/grit/grit/gather/interface.py
new file mode 100644
index 0000000000..15d64f9326
--- /dev/null
+++ b/tools/grit/grit/gather/interface.py
@@ -0,0 +1,172 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Interface for all gatherers.
+'''
+
+from __future__ import print_function
+
+import os.path
+
+import six
+
+from grit import clique
+from grit import util
+
+
+class GathererBase(object):
+ '''Interface for all gatherer implementations. Subclasses must implement
+ all methods that raise NotImplemented.'''
+
+ def __init__(self, rc_file, extkey=None, encoding='cp1252', is_skeleton=False):
+ '''Initializes the gatherer object's attributes, but does not attempt to
+ read the input file.
+
+ Args:
+ rc_file: The 'file' attribute of the <structure> node (usually the
+ relative path to the source file).
+ extkey: e.g. 'ID_MY_DIALOG'
+ encoding: e.g. 'utf-8'
+ is_skeleton: Indicates whether this gatherer is a skeleton gatherer, in
+ which case we should not do some types of processing on the
+ translateable bits.
+ '''
+ self.rc_file = rc_file
+ self.extkey = extkey
+ self.encoding = encoding
+ # A default uberclique that is local to this object. Users can override
+ # this with the uberclique they are using.
+ self.uberclique = clique.UberClique()
+ # Indicates whether this gatherer is a skeleton gatherer, in which case
+ # we should not do some types of processing on the translateable bits.
+ self.is_skeleton = is_skeleton
+ # Stores the grd node on which this gatherer is running. This allows
+ # evaluating expressions.
+ self.grd_node = None
+
+ def SetAttributes(self, attrs):
+ '''Sets node attributes used by the gatherer.
+
+ By default, this does nothing. If special handling is desired, it should be
+ overridden by the child gatherer.
+
+ Args:
+ attrs: The mapping of node attributes.
+ '''
+ pass
+
+ def SetDefines(self, defines):
+ '''Sets global defines used by the gatherer.
+
+ By default, this does nothing. If special handling is desired, it should be
+ overridden by the child gatherer.
+
+ Args:
+ defines: The mapping of define values.
+ '''
+ pass
+
+ def SetGrdNode(self, node):
+ '''Sets the grd node on which this gatherer is running.
+ '''
+ self.grd_node = node
+
+ def SetUberClique(self, uberclique):
+ '''Overrides the default uberclique so that cliques created by this object
+ become part of the uberclique supplied by the user.
+ '''
+ self.uberclique = uberclique
+
+ def Parse(self):
+ '''Reads and parses the contents of what is being gathered.'''
+ raise NotImplementedError()
+
+ def GetData(self, lang, encoding):
+ '''Returns the data to be added to the DataPack for this node or None if
+ this node does not add a DataPack entry.
+ '''
+ return None
+
+ def GetText(self):
+ '''Returns the text of what is being gathered.'''
+ raise NotImplementedError()
+
+ def GetTextualIds(self):
+ '''Returns the mnemonic IDs that need to be defined for the resource
+ being gathered to compile correctly.'''
+ return []
+
+ def GetCliques(self):
+ '''Returns the MessageClique objects for all translateable portions.'''
+ return []
+
+ def GetInputPath(self):
+ return self.rc_file
+
+ def GetHtmlResourceFilenames(self):
+ """Returns a set of all filenames inlined by this gatherer."""
+ return []
+
+ def Translate(self, lang, pseudo_if_not_available=True,
+ skeleton_gatherer=None, fallback_to_english=False):
+ '''Returns the resource being gathered, with translateable portions filled
+ with the translation for language 'lang'.
+
+ If pseudo_if_not_available is true, a pseudotranslation will be used for any
+ message that doesn't have a real translation available.
+
+ If no translation is available and pseudo_if_not_available is false,
+ fallback_to_english controls the behavior. If it is false, throw an error.
+ If it is true, use the English version of the message as its own
+ "translation".
+
+ If skeleton_gatherer is specified, the translation will use the nontranslateable
+ parts from the gatherer 'skeleton_gatherer', which must be of the same type
+ as 'self'.
+
+ If fallback_to_english
+
+ Args:
+ lang: 'en'
+ pseudo_if_not_available: True | False
+ skeleton_gatherer: other_gatherer
+ fallback_to_english: True | False
+
+ Return:
+ e.g. 'ID_THIS_SECTION TYPE\n...BEGIN\n "Translated message"\n......\nEND'
+
+ Raises:
+ grit.exception.NotReady() if used before Parse() has been successfully
+ called.
+ grit.exception.NoSuchTranslation() if 'pseudo_if_not_available' and
+ fallback_to_english are both false and there is no translation for the
+ requested language.
+ '''
+ raise NotImplementedError()
+
+ def SubstituteMessages(self, substituter):
+ '''Applies substitutions to all messages in the gatherer.
+
+ Args:
+ substituter: a grit.util.Substituter object.
+ '''
+ pass
+
+ def SetFilenameExpansionFunction(self, fn):
+ '''Sets a function for rewriting filenames before gathering.'''
+ pass
+
+ # TODO(benrg): Move this elsewhere, since it isn't part of the interface.
+ def _LoadInputFile(self):
+ '''A convenience function for subclasses that loads the contents of the
+ input file.
+ '''
+ if isinstance(self.rc_file, six.string_types):
+ path = self.GetInputPath()
+ # Hack: some unit tests supply an absolute path and no root node.
+ if not os.path.isabs(path):
+ path = self.grd_node.ToRealPath(path)
+ return util.ReadFile(path, self.encoding)
+ else:
+ return self.rc_file.read()
diff --git a/tools/grit/grit/gather/json_loader.py b/tools/grit/grit/gather/json_loader.py
new file mode 100644
index 0000000000..058e5f17ae
--- /dev/null
+++ b/tools/grit/grit/gather/json_loader.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import print_function
+
+from grit.gather import interface
+
+
+class JsonLoader(interface.GathererBase):
+ '''A simple gatherer that loads and parses a JSON file.'''
+
+ def Parse(self):
+ '''Reads and parses the text of self._json_text into the data structure in
+ self._data.
+ '''
+ self._json_text = self._LoadInputFile()
+ self._data = None
+
+ globs = {}
+ exec('data = ' + self._json_text, globs)
+ self._data = globs['data']
+
+ def GetData(self, lang, encoding):
+ '''Returns the parsed JSON data.'''
+ assert encoding == 'utf-8'
+ return self._data
diff --git a/tools/grit/grit/gather/policy_json.py b/tools/grit/grit/gather/policy_json.py
new file mode 100644
index 0000000000..6621c5f3c4
--- /dev/null
+++ b/tools/grit/grit/gather/policy_json.py
@@ -0,0 +1,325 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Support for "policy_templates.json" format used by the policy template
+generator as a source for generating ADM,ADMX,etc files.'''
+
+from __future__ import print_function
+
+import json
+import sys
+
+import six
+
+from grit.gather import skeleton_gatherer
+from grit import util
+from grit import tclib
+from xml.dom import minidom
+from xml.parsers.expat import ExpatError
+
+
+class PolicyJson(skeleton_gatherer.SkeletonGatherer):
+ '''Collects and translates the following strings from policy_templates.json:
+ - captions, descriptions, labels and Android app support details of policies
+ - captions of enumeration items
+ - misc strings from the 'messages' section
+ Translatable strings may have untranslateable placeholders with the same
+ format that is used in .grd files.
+ '''
+
+ def _AddEndline(self, add_comma):
+ '''Adds an endline to the skeleton tree. If add_comma is true, adds a
+ comma before the endline.
+
+ Args:
+ add_comma: A boolean to add a comma or not.
+ '''
+ self._AddNontranslateableChunk(',\n' if add_comma else '\n')
+
+ def _ParsePlaceholder(self, placeholder, msg):
+ '''Extracts a placeholder from a DOM node and adds it to a tclib Message.
+
+ Args:
+ placeholder: A DOM node of the form:
+ <ph name="PLACEHOLDER_NAME">Placeholder text<ex>Example value</ex></ph>
+ msg: The placeholder is added to this message.
+ '''
+ text = []
+ example_text = []
+ for node1 in placeholder.childNodes:
+ if (node1.nodeType == minidom.Node.TEXT_NODE):
+ text.append(node1.data)
+ elif (node1.nodeType == minidom.Node.ELEMENT_NODE and
+ node1.tagName == 'ex'):
+ for node2 in node1.childNodes:
+ example_text.append(node2.toxml())
+ else:
+ raise Exception('Unexpected element inside a placeholder: ' +
+ node2.toxml())
+ if example_text == []:
+ # In such cases the original text is okay for an example.
+ example_text = text
+
+ replaced_text = self.Escape(''.join(text).strip())
+ replaced_text = replaced_text.replace('$1', self._config['app_name'])
+ replaced_text = replaced_text.replace('$2', self._config['os_name'])
+ replaced_text = replaced_text.replace('$3', self._config['frame_name'])
+
+ msg.AppendPlaceholder(tclib.Placeholder(
+ placeholder.attributes['name'].value,
+ replaced_text,
+ ''.join(example_text).strip()))
+
+ def _ParseMessage(self, string, desc):
+ '''Parses a given string and adds it to the output as a translatable chunk
+ with a given description.
+
+ Args:
+ string: The message string to parse.
+ desc: The description of the message (for the translators).
+ '''
+ msg = tclib.Message(description=desc)
+ xml = '<msg>' + string + '</msg>'
+ try:
+ node = minidom.parseString(xml).childNodes[0]
+ except ExpatError:
+ reason = '''Input isn't valid XML (has < & > been escaped?): ''' + string
+ six.reraise(Exception, reason, sys.exc_info()[2])
+
+ for child in node.childNodes:
+ if child.nodeType == minidom.Node.TEXT_NODE:
+ msg.AppendText(child.data)
+ elif child.nodeType == minidom.Node.ELEMENT_NODE:
+ if child.tagName == 'ph':
+ self._ParsePlaceholder(child, msg)
+ else:
+ raise Exception("Not implemented.")
+ else:
+ raise Exception("Not implemented.")
+ self.skeleton_.append(self.uberclique.MakeClique(msg))
+
+ def _ParseNode(self, node):
+ '''Traverses the subtree of a DOM node, and register a tclib message for
+ all the <message> nodes.
+ '''
+ att_text = []
+ if node.attributes:
+ for key, value in sorted(node.attributes.items()):
+ att_text.append(' %s=\"%s\"' % (key, value))
+ self._AddNontranslateableChunk("<%s%s>" %
+ (node.tagName, ''.join(att_text)))
+ if node.tagName == 'message':
+ msg = tclib.Message(description=node.attributes['desc'])
+ for child in node.childNodes:
+ if child.nodeType == minidom.Node.TEXT_NODE:
+ if msg == None:
+ self._AddNontranslateableChunk(child.data)
+ else:
+ msg.AppendText(child.data)
+ elif child.nodeType == minidom.Node.ELEMENT_NODE:
+ if child.tagName == 'ph':
+ self._ParsePlaceholder(child, msg)
+ else:
+ assert False
+ self.skeleton_.append(self.uberclique.MakeClique(msg))
+ else:
+ for child in node.childNodes:
+ if child.nodeType == minidom.Node.TEXT_NODE:
+ self._AddNontranslateableChunk(child.data)
+ elif node.nodeType == minidom.Node.ELEMENT_NODE:
+ self._ParseNode(child)
+
+ self._AddNontranslateableChunk("</%s>" % node.tagName)
+
+ def _AddIndentedNontranslateableChunk(self, depth, string):
+ '''Adds a nontranslateable chunk of text to the internally stored output.
+
+ Args:
+ depth: The number of double spaces to prepend to the next argument string.
+ string: The chunk of text to add.
+ '''
+ result = []
+ while depth > 0:
+ result.append(' ')
+ depth = depth - 1
+ result.append(string)
+ self._AddNontranslateableChunk(''.join(result))
+
+ def _GetDescription(self, item, item_type, parent_item, key):
+ '''Creates a description for a translatable message. The description gives
+ some context for the person who will translate this message.
+
+ Args:
+ item: A policy or an enumeration item.
+ item_type: 'enum_item' | 'policy'
+ parent_item: The owner of item. (A policy of type group or enum.)
+ key: The name of the key to parse.
+ depth: The level of indentation.
+ '''
+ key_map = {
+ 'desc': 'Description',
+ 'caption': 'Caption',
+ 'label': 'Label',
+ 'arc_support': 'Information about the effect on Android apps'
+ }
+ if item_type == 'policy':
+ return ('%s of the policy named %s [owner(s): %s]' %
+ (key_map[key], item['name'],
+ ','.join(item['owners'] if 'owners' in item else 'unknown')))
+ if item_type == 'enum_item':
+ return ('%s of the option named %s in policy %s [owner(s): %s]' %
+ (key_map[key], item['name'], parent_item['name'],
+ ','.join(parent_item['owners'] if 'owners' in parent_item else 'unknown')))
+ raise Exception('Unexpected type %s' % item_type)
+
+ def _AddSchemaKeys(self, obj, depth):
+ obj_type = type(obj)
+ if obj_type == dict:
+ self._AddNontranslateableChunk('{\n')
+ keys = sorted(obj.keys())
+ for count, (key) in enumerate(keys, 1):
+ json_key = "%s: " % json.dumps(key)
+ self._AddIndentedNontranslateableChunk(depth + 1, json_key)
+ if key == 'description' and type(obj[key]) == str:
+ self._AddNontranslateableChunk("\"")
+ self._ParseMessage(obj[key], 'Description of schema property')
+ self._AddNontranslateableChunk("\"")
+ elif type(obj[key]) in (bool, int, str):
+ self._AddSchemaKeys(obj[key], 0)
+ else:
+ self._AddSchemaKeys(obj[key], depth + 1)
+ self._AddEndline(count < len(keys))
+ self._AddIndentedNontranslateableChunk(depth, '}')
+ elif obj_type == list:
+ self._AddNontranslateableChunk('[\n')
+ for count, (item) in enumerate(obj, 1):
+ self._AddSchemaKeys(item, depth + 1)
+ self._AddEndline(count < len(obj))
+ self._AddIndentedNontranslateableChunk(depth, ']')
+ elif obj_type in (bool, int, str):
+ self._AddIndentedNontranslateableChunk(depth, json.dumps(obj))
+ else:
+ raise Exception('Invalid schema object: %s' % obj)
+
+ def _AddPolicyKey(self, item, item_type, parent_item, key, depth):
+ '''Given a policy/enumeration item and a key, adds that key and its value
+ into the output.
+ E.g.:
+ 'example_value': 123
+ If key indicates that the value is a translatable string, then it is parsed
+ as a translatable string.
+
+ Args:
+ item: A policy or an enumeration item.
+ item_type: 'enum_item' | 'policy'
+ parent_item: The owner of item. (A policy of type group or enum.)
+ key: The name of the key to parse.
+ depth: The level of indentation.
+ '''
+ self._AddIndentedNontranslateableChunk(depth, "%s: " % json.dumps(key))
+ if key in ('desc', 'caption', 'label', 'arc_support'):
+ self._AddNontranslateableChunk("\"")
+ self._ParseMessage(
+ item[key],
+ self._GetDescription(item, item_type, parent_item, key))
+ self._AddNontranslateableChunk("\"")
+ elif key in ('schema', 'validation_schema', 'description_schema'):
+ self._AddSchemaKeys(item[key], depth)
+ else:
+ self._AddNontranslateableChunk(json.dumps(item[key], ensure_ascii=False))
+
+ def _AddItems(self, items, item_type, parent_item, depth):
+ '''Parses and adds a list of items from the JSON file. Items can be policies
+ or parts of an enum policy.
+
+ Args:
+ items: Either a list of policies or a list of dictionaries.
+ item_type: 'enum_item' | 'policy'
+ parent_item: If items contains a list of policies, then this is the policy
+ group that owns them. If items contains a list of enumeration items,
+ then this is the enum policy that holds them.
+ depth: Indicates the depth of our position in the JSON hierarchy. Used to
+ add nice line-indent to the output.
+ '''
+ for item_count, (item1) in enumerate(items, 1):
+ self._AddIndentedNontranslateableChunk(depth, "{\n")
+ keys = sorted(item1.keys())
+ for keys_count, (key) in enumerate(keys, 1):
+ if key == 'items':
+ self._AddIndentedNontranslateableChunk(depth + 1, "\"items\": [\n")
+ self._AddItems(item1['items'], 'enum_item', item1, depth + 2)
+ self._AddIndentedNontranslateableChunk(depth + 1, "]")
+ elif key == 'policies' and all(not isinstance(x, str)
+ for x in item1['policies']):
+ self._AddIndentedNontranslateableChunk(depth + 1, "\"policies\": [\n")
+ self._AddItems(item1['policies'], 'policy', item1, depth + 2)
+ self._AddIndentedNontranslateableChunk(depth + 1, "]")
+ else:
+ self._AddPolicyKey(item1, item_type, parent_item, key, depth + 1)
+ self._AddEndline(keys_count < len(keys))
+ self._AddIndentedNontranslateableChunk(depth, "}")
+ self._AddEndline(item_count < len(items))
+
+ def _AddMessages(self):
+ '''Processed and adds the 'messages' section to the output.'''
+ self._AddNontranslateableChunk(" \"messages\": {\n")
+ messages = self.data['messages'].items()
+ for count, (name, message) in enumerate(messages, 1):
+ self._AddNontranslateableChunk(" %s: {\n" % json.dumps(name))
+ self._AddNontranslateableChunk(" \"text\": \"")
+ self._ParseMessage(message['text'], message['desc'])
+ self._AddNontranslateableChunk("\"\n")
+ self._AddNontranslateableChunk(" }")
+ self._AddEndline(count < len(self.data['messages']))
+ self._AddNontranslateableChunk(" }\n")
+
+ # Although we use the RegexpGatherer base class, we do not use the
+ # _RegExpParse method of that class to implement Parse(). Instead, we
+ # parse using a DOM parser.
+ def Parse(self):
+ if self.have_parsed_:
+ return
+ self.have_parsed_ = True
+
+ self.text_ = self._LoadInputFile()
+ if util.IsExtraVerbose():
+ print(self.text_)
+
+ self.data = eval(self.text_)
+
+ self._AddNontranslateableChunk('{\n')
+ self._AddNontranslateableChunk(" \"policy_definitions\": [\n")
+ self._AddItems(self.data['policy_definitions'], 'policy', None, 2)
+ self._AddNontranslateableChunk(" ],\n")
+ self._AddNontranslateableChunk(" \"policy_atomic_group_definitions\": [\n")
+ if 'policy_atomic_group_definitions' in self.data:
+ self._AddItems(self.data['policy_atomic_group_definitions'],
+ 'policy', None, 2)
+ self._AddNontranslateableChunk(" ],\n")
+ self._AddMessages()
+ self._AddNontranslateableChunk('\n}')
+
+ def Escape(self, text):
+ return json.dumps(text, ensure_ascii=False)[1:-1]
+
+ def SetDefines(self, defines):
+ if not defines:
+ raise Exception('Must pass valid defines')
+
+ if '_chromium' in defines:
+ self._config = {
+ 'build': 'chromium',
+ 'app_name': 'Chromium',
+ 'frame_name': 'Chromium Frame',
+ 'os_name': 'Chromium OS',
+ }
+ elif '_google_chrome' in defines:
+ self._config = {
+ 'build': 'chrome',
+ 'app_name': 'Google Chrome',
+ 'frame_name': 'Google Chrome Frame',
+ 'os_name': 'Google Chrome OS',
+ }
+ else:
+ raise Exception('Unknown build')
diff --git a/tools/grit/grit/gather/policy_json_unittest.py b/tools/grit/grit/gather/policy_json_unittest.py
new file mode 100644
index 0000000000..214cd276aa
--- /dev/null
+++ b/tools/grit/grit/gather/policy_json_unittest.py
@@ -0,0 +1,347 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.policy_json'''
+
+from __future__ import print_function
+
+import json
+import os
+import re
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import policy_json
+
+class PolicyJsonUnittest(unittest.TestCase):
+
+ def GetExpectedOutput(self, original):
+ expected = eval(original)
+ for key, message in expected['messages'].items():
+ del message['desc']
+ return expected
+
+ def testEmpty(self):
+ original = """{
+ 'policy_definitions': [],
+ 'policy_atomic_group_definitions': [],
+ 'messages': {}
+ }"""
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 0)
+ self.failUnless(eval(original) == json.loads(gatherer.Translate('en')))
+
+ def testGeneralPolicy(self):
+ original = (
+ "{"
+ " 'policy_definitions': ["
+ " {"
+ " 'name': 'HomepageLocation',"
+ " 'type': 'string',"
+ " 'owners': ['foo@bar.com'],"
+ " 'supported_on': ['chrome.*:8-'],"
+ " 'features': {'dynamic_refresh': 1},"
+ " 'example_value': 'http://chromium.org',"
+ " 'caption': 'nothing special 1',"
+ " 'desc': 'nothing special 2',"
+ " 'label': 'nothing special 3',"
+ " },"
+ " ],"
+ " 'policy_atomic_group_definitions': [],"
+ " 'messages': {"
+ " 'msg_identifier': {"
+ " 'text': 'nothing special 3',"
+ " 'desc': 'nothing special descr 3',"
+ " }"
+ " }"
+ "}")
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 4)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ def testEnum(self):
+ original = (
+ "{"
+ " 'policy_definitions': ["
+ " {"
+ " 'name': 'Policy1',"
+ " 'owners': ['a@b'],"
+ " 'items': ["
+ " {"
+ " 'name': 'Item1',"
+ " 'caption': 'nothing special',"
+ " }"
+ " ]"
+ " },"
+ " ],"
+ " 'policy_atomic_group_definitions': [],"
+ " 'messages': {}"
+ "}")
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ def testSchema(self):
+ original = ("{"
+ " 'policy_definitions': ["
+ " {"
+ " 'name': 'Policy1',"
+ " 'schema': {"
+ " 'type': 'object',"
+ " 'properties': {"
+ " 'outer': {"
+ " 'description': 'outer description',"
+ " 'type': 'object',"
+ " 'inner': {"
+ " 'description': 'inner description',"
+ " 'type': 'integer', 'minimum': 0, 'maximum': 100"
+ " },"
+ " 'inner2': {"
+ " 'description': 'inner2 description',"
+ " 'type': 'integer',"
+ " 'enum': [ 1, 2, 3 ],"
+ " 'sensitiveValue': True"
+ " },"
+ " },"
+ " },"
+ " },"
+ " 'caption': 'nothing special',"
+ " 'owners': ['a@b']"
+ " },"
+ " ],"
+ " 'policy_atomic_group_definitions': [],"
+ " 'messages': {}"
+ "}")
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 4)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ def testValidationSchema(self):
+ original = ("{"
+ " 'policy_definitions': ["
+ " {"
+ " 'name': 'Policy1',"
+ " 'owners': ['a@b'],"
+ " 'validation_schema': {"
+ " 'type': 'object',"
+ " 'properties': {"
+ " 'description': 'properties description',"
+ " 'type': 'object',"
+ " },"
+ " },"
+ " },"
+ " ],"
+ " 'policy_atomic_group_definitions': [],"
+ " 'messages': {}"
+ "}")
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ def testDescriptionSchema(self):
+ original = ("{"
+ " 'policy_definitions': ["
+ " {"
+ " 'name': 'Policy1',"
+ " 'owners': ['a@b'],"
+ " 'description_schema': {"
+ " 'type': 'object',"
+ " 'properties': {"
+ " 'description': 'properties description',"
+ " 'type': 'object',"
+ " },"
+ " },"
+ " },"
+ " ],"
+ " 'policy_atomic_group_definitions': [],"
+ " 'messages': {}"
+ "}")
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ # Keeping for backwards compatibility.
+ def testSubPolicyOldFormat(self):
+ original = (
+ "{"
+ " 'policy_definitions': ["
+ " {"
+ " 'type': 'group',"
+ " 'policies': ["
+ " {"
+ " 'name': 'Policy1',"
+ " 'caption': 'nothing special',"
+ " 'owners': ['a@b']"
+ " }"
+ " ]"
+ " }"
+ " ],"
+ " 'policy_atomic_group_definitions': [],"
+ " 'messages': {}"
+ "}")
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ def testSubPolicyNewFormat(self):
+ original = (
+ "{"
+ " 'policy_definitions': ["
+ " {"
+ " 'type': 'group',"
+ " 'policies': ['Policy1']"
+ " },"
+ " {"
+ " 'name': 'Policy1',"
+ " 'caption': 'nothing special',"
+ " 'owners': ['a@b']"
+ " }"
+ " ],"
+ " 'policy_atomic_group_definitions': [],"
+ " 'messages': {}"
+ "}")
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ def testEscapingAndLineBreaks(self):
+ original = """{
+ 'policy_definitions': [],
+ 'policy_atomic_group_definitions': [],
+ 'messages': {
+ 'msg1': {
+ # The following line will contain two backslash characters when it
+ # ends up in eval().
+ 'text': '''backslashes, Sir? \\\\''',
+ 'desc': ''
+ },
+ 'msg2': {
+ 'text': '''quotes, Madam? "''',
+ 'desc': ''
+ },
+ 'msg3': {
+ # The following line will contain two backslash characters when it
+ # ends up in eval().
+ 'text': 'backslashes, Sir? \\\\',
+ 'desc': ''
+ },
+ 'msg4': {
+ 'text': "quotes, Madam? '",
+ 'desc': ''
+ },
+ 'msg5': {
+ 'text': '''what happens
+with a newline?''',
+ 'desc': ''
+ },
+ 'msg6': {
+ # The following line will contain a backslash+n when it ends up in
+ # eval().
+ 'text': 'what happens\\nwith a newline? (Episode 1)',
+ 'desc': ''
+ }
+ }
+}"""
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 6)
+ expected = self.GetExpectedOutput(original)
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+ def testPlaceholdersChromium(self):
+ original = """{
+ "policy_definitions": [
+ {
+ "name": "Policy1",
+ "caption": "Please install\\n<ph name=\\"PRODUCT_NAME\\">$1<ex>Google Chrome</ex></ph>.",
+ "owners": "a@b"
+ }
+ ],
+ "policy_atomic_group_definitions": [],
+ "messages": {}
+}"""
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.SetDefines({'_chromium': True})
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ expected = json.loads(re.sub('<ph.*ph>', 'Chromium', original))
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+ self.failUnless(gatherer.GetCliques()[0].translateable)
+ msg = gatherer.GetCliques()[0].GetMessage()
+ self.failUnless(len(msg.GetPlaceholders()) == 1)
+ ph = msg.GetPlaceholders()[0]
+ self.failUnless(ph.GetOriginal() == 'Chromium')
+ self.failUnless(ph.GetPresentation() == 'PRODUCT_NAME')
+ self.failUnless(ph.GetExample() == 'Google Chrome')
+
+ def testPlaceholdersChrome(self):
+ original = """{
+ "policy_definitions": [
+ {
+ "name": "Policy1",
+ "caption": "Please install\\n<ph name=\\"PRODUCT_NAME\\">$1<ex>Google Chrome</ex></ph>.",
+ "owners": "a@b"
+ }
+ ],
+ "policy_atomic_group_definitions": [],
+ "messages": {}
+}"""
+ gatherer = policy_json.PolicyJson(StringIO(original))
+ gatherer.SetDefines({'_google_chrome': True})
+ gatherer.Parse()
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ expected = json.loads(re.sub('<ph.*ph>', 'Google Chrome', original))
+ self.failUnless(expected == json.loads(gatherer.Translate('en')))
+ self.failUnless(gatherer.GetCliques()[0].translateable)
+ msg = gatherer.GetCliques()[0].GetMessage()
+ self.failUnless(len(msg.GetPlaceholders()) == 1)
+ ph = msg.GetPlaceholders()[0]
+ self.failUnless(ph.GetOriginal() == 'Google Chrome')
+ self.failUnless(ph.GetPresentation() == 'PRODUCT_NAME')
+ self.failUnless(ph.GetExample() == 'Google Chrome')
+
+ def testGetDescription(self):
+ gatherer = policy_json.PolicyJson({})
+ gatherer.SetDefines({'_google_chrome': True})
+ self.assertEquals(
+ gatherer._GetDescription({'name': 'Policy1', 'owners': ['a@b']},
+ 'policy', None, 'desc'),
+ 'Description of the policy named Policy1 [owner(s): a@b]')
+ self.assertEquals(
+ gatherer._GetDescription({'name': 'Plcy2', 'owners': ['a@b', 'c@d']},
+ 'policy', None, 'caption'),
+ 'Caption of the policy named Plcy2 [owner(s): a@b,c@d]')
+ self.assertEquals(
+ gatherer._GetDescription({'name': 'Plcy3', 'owners': ['a@b']},
+ 'policy', None, 'label'),
+ 'Label of the policy named Plcy3 [owner(s): a@b]')
+ self.assertEquals(
+ gatherer._GetDescription({'name': 'Item'}, 'enum_item',
+ {'name': 'Plcy', 'owners': ['a@b']}, 'caption'),
+ 'Caption of the option named Item in policy Plcy [owner(s): a@b]')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/gather/rc.py b/tools/grit/grit/gather/rc.py
new file mode 100644
index 0000000000..dd091d1e18
--- /dev/null
+++ b/tools/grit/grit/gather/rc.py
@@ -0,0 +1,343 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Support for gathering resources from RC files.
+'''
+
+from __future__ import print_function
+
+import re
+
+from grit import exception
+from grit import lazy_re
+from grit import tclib
+
+from grit.gather import regexp
+
+
+# Find portions that need unescaping in resource strings. We need to be
+# careful that a \\n is matched _first_ as a \\ rather than matching as
+# a \ followed by a \n.
+# TODO(joi) Handle ampersands if we decide to change them into <ph>
+# TODO(joi) May need to handle other control characters than \n
+_NEED_UNESCAPE = lazy_re.compile(r'""|\\\\|\\n|\\t')
+
+# Find portions that need escaping to encode string as a resource string.
+_NEED_ESCAPE = lazy_re.compile(r'"|\n|\t|\\|\ \;')
+
+# How to escape certain characters
+_ESCAPE_CHARS = {
+ '"' : '""',
+ '\n' : '\\n',
+ '\t' : '\\t',
+ '\\' : '\\\\',
+ ' ' : ' '
+}
+
+# How to unescape certain strings
+_UNESCAPE_CHARS = dict([[value, key] for key, value in _ESCAPE_CHARS.items()])
+
+
+
+class Section(regexp.RegexpGatherer):
+ '''A section from a resource file.'''
+
+ @staticmethod
+ def Escape(text):
+ '''Returns a version of 'text' with characters escaped that need to be
+ for inclusion in a resource section.'''
+ def Replace(match):
+ return _ESCAPE_CHARS[match.group()]
+ return _NEED_ESCAPE.sub(Replace, text)
+
+ @staticmethod
+ def UnEscape(text):
+ '''Returns a version of 'text' with escaped characters unescaped.'''
+ def Replace(match):
+ return _UNESCAPE_CHARS[match.group()]
+ return _NEED_UNESCAPE.sub(Replace, text)
+
+ def _RegExpParse(self, rexp, text_to_parse):
+ '''Overrides _RegExpParse to add shortcut group handling. Otherwise
+ the same.
+ '''
+ super(Section, self)._RegExpParse(rexp, text_to_parse)
+
+ if not self.is_skeleton and len(self.GetTextualIds()) > 0:
+ group_name = self.GetTextualIds()[0]
+ for c in self.GetCliques():
+ c.AddToShortcutGroup(group_name)
+
+ def ReadSection(self):
+ rc_text = self._LoadInputFile()
+
+ out = ''
+ begin_count = 0
+ assert self.extkey
+ first_line_re = re.compile(r'\s*' + self.extkey + r'\b')
+ for line in rc_text.splitlines(True):
+ if out or first_line_re.match(line):
+ out += line
+
+ # we stop once we reach the END for the outermost block.
+ begin_count_was = begin_count
+ if len(out) > 0 and line.strip() == 'BEGIN':
+ begin_count += 1
+ elif len(out) > 0 and line.strip() == 'END':
+ begin_count -= 1
+ if begin_count_was == 1 and begin_count == 0:
+ break
+
+ if len(out) == 0:
+ raise exception.SectionNotFound('%s in file %s' % (self.extkey, self.rc_file))
+
+ self.text_ = out.strip()
+
+
+class Dialog(Section):
+ '''A resource section that contains a dialog resource.'''
+
+ # A typical dialog resource section looks like this:
+ #
+ # IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+ # STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+ # CAPTION "About"
+ # FONT 8, "System", 0, 0, 0x0
+ # BEGIN
+ # ICON IDI_KLONK,IDC_MYICON,14,9,20,20
+ # LTEXT "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+ # SS_NOPREFIX
+ # LTEXT "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+ # DEFPUSHBUTTON "OK",IDOK,195,6,30,11,WS_GROUP
+ # CONTROL "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+ # BS_AUTORADIOBUTTON,46,51,84,10
+ # END
+
+ # We are using a sorted set of keys, and we assume that the
+ # group name used for descriptions (type) will come after the "text"
+ # group in alphabetical order. We also assume that there cannot be
+ # more than one description per regular expression match.
+ # If that's not the case some descriptions will be clobbered.
+ dialog_re_ = lazy_re.compile(r'''
+ # The dialog's ID in the first line
+ (?P<id1>[A-Z0-9_]+)\s+DIALOG(EX)?
+ |
+ # The caption of the dialog
+ (?P<type1>CAPTION)\s+"(?P<text1>.*?([^"]|""))"\s
+ |
+ # Lines for controls that have text and an ID
+ \s+(?P<type2>[A-Z]+)\s+"(?P<text2>.*?([^"]|"")?)"\s*,\s*(?P<id2>[A-Z0-9_]+)\s*,
+ |
+ # Lines for controls that have text only
+ \s+(?P<type3>[A-Z]+)\s+"(?P<text3>.*?([^"]|"")?)"\s*,
+ |
+ # Lines for controls that reference other resources
+ \s+[A-Z]+\s+[A-Z0-9_]+\s*,\s*(?P<id3>[A-Z0-9_]*[A-Z][A-Z0-9_]*)
+ |
+ # This matches "NOT SOME_STYLE" so that it gets consumed and doesn't get
+ # matched by the next option (controls that have only an ID and then just
+ # numbers)
+ \s+NOT\s+[A-Z][A-Z0-9_]+
+ |
+ # Lines for controls that have only an ID and then just numbers
+ \s+[A-Z]+\s+(?P<id4>[A-Z0-9_]*[A-Z][A-Z0-9_]*)\s*,
+ ''', re.MULTILINE | re.VERBOSE)
+
+ def Parse(self):
+ '''Knows how to parse dialog resource sections.'''
+ self.ReadSection()
+ self._RegExpParse(self.dialog_re_, self.text_)
+
+
+class Menu(Section):
+ '''A resource section that contains a menu resource.'''
+
+ # A typical menu resource section looks something like this:
+ #
+ # IDC_KLONK MENU
+ # BEGIN
+ # POPUP "&File"
+ # BEGIN
+ # MENUITEM "E&xit", IDM_EXIT
+ # MENUITEM "This be ""Klonk"" me like", ID_FILE_THISBE
+ # POPUP "gonk"
+ # BEGIN
+ # MENUITEM "Klonk && is ""good""", ID_GONK_KLONKIS
+ # END
+ # END
+ # POPUP "&Help"
+ # BEGIN
+ # MENUITEM "&About ...", IDM_ABOUT
+ # END
+ # END
+
+ # Description used for the messages generated for menus, to explain to
+ # the translators how to handle them.
+ MENU_MESSAGE_DESCRIPTION = (
+ 'This message represents a menu. Each of the items appears in sequence '
+ '(some possibly within sub-menus) in the menu. The XX01XX placeholders '
+ 'serve to separate items. Each item contains an & (ampersand) character '
+ 'in front of the keystroke that should be used as a shortcut for that item '
+ 'in the menu. Please make sure that no two items in the same menu share '
+ 'the same shortcut.'
+ )
+
+ # A dandy regexp to suck all the IDs and translateables out of a menu
+ # resource
+ menu_re_ = lazy_re.compile(r'''
+ # Match the MENU ID on the first line
+ ^(?P<id1>[A-Z0-9_]+)\s+MENU
+ |
+ # Match the translateable caption for a popup menu
+ POPUP\s+"(?P<text1>.*?([^"]|""))"\s
+ |
+ # Match the caption & ID of a MENUITEM
+ MENUITEM\s+"(?P<text2>.*?([^"]|""))"\s*,\s*(?P<id2>[A-Z0-9_]+)
+ ''', re.MULTILINE | re.VERBOSE)
+
+ def Parse(self):
+ '''Knows how to parse menu resource sections. Because it is important that
+ menu shortcuts are unique within the menu, we return each menu as a single
+ message with placeholders to break up the different menu items, rather than
+ return a single message per menu item. we also add an automatic description
+ with instructions for the translators.'''
+ self.ReadSection()
+ self.single_message_ = tclib.Message(description=self.MENU_MESSAGE_DESCRIPTION)
+ self._RegExpParse(self.menu_re_, self.text_)
+
+
+class Version(Section):
+ '''A resource section that contains a VERSIONINFO resource.'''
+
+ # A typical version info resource can look like this:
+ #
+ # VS_VERSION_INFO VERSIONINFO
+ # FILEVERSION 1,0,0,1
+ # PRODUCTVERSION 1,0,0,1
+ # FILEFLAGSMASK 0x3fL
+ # #ifdef _DEBUG
+ # FILEFLAGS 0x1L
+ # #else
+ # FILEFLAGS 0x0L
+ # #endif
+ # FILEOS 0x4L
+ # FILETYPE 0x2L
+ # FILESUBTYPE 0x0L
+ # BEGIN
+ # BLOCK "StringFileInfo"
+ # BEGIN
+ # BLOCK "040904e4"
+ # BEGIN
+ # VALUE "CompanyName", "TODO: <Company name>"
+ # VALUE "FileDescription", "TODO: <File description>"
+ # VALUE "FileVersion", "1.0.0.1"
+ # VALUE "LegalCopyright", "TODO: (c) <Company name>. All rights reserved."
+ # VALUE "InternalName", "res_format_test.dll"
+ # VALUE "OriginalFilename", "res_format_test.dll"
+ # VALUE "ProductName", "TODO: <Product name>"
+ # VALUE "ProductVersion", "1.0.0.1"
+ # END
+ # END
+ # BLOCK "VarFileInfo"
+ # BEGIN
+ # VALUE "Translation", 0x409, 1252
+ # END
+ # END
+ #
+ #
+ # In addition to the above fields, VALUE fields named "Comments" and
+ # "LegalTrademarks" may also be translateable.
+
+ version_re_ = lazy_re.compile(r'''
+ # Match the ID on the first line
+ ^(?P<id1>[A-Z0-9_]+)\s+VERSIONINFO
+ |
+ # Match all potentially translateable VALUE sections
+ \s+VALUE\s+"
+ (
+ CompanyName|FileDescription|LegalCopyright|
+ ProductName|Comments|LegalTrademarks
+ )",\s+"(?P<text1>.*?([^"]|""))"\s
+ ''', re.MULTILINE | re.VERBOSE)
+
+ def Parse(self):
+ '''Knows how to parse VERSIONINFO resource sections.'''
+ self.ReadSection()
+ self._RegExpParse(self.version_re_, self.text_)
+
+ # TODO(joi) May need to override the Translate() method to change the
+ # "Translation" VALUE block to indicate the correct language code.
+
+
+class RCData(Section):
+ '''A resource section that contains some data .'''
+
+ # A typical rcdataresource section looks like this:
+ #
+ # IDR_BLAH RCDATA { 1, 2, 3, 4 }
+
+ dialog_re_ = lazy_re.compile(r'''
+ ^(?P<id1>[A-Z0-9_]+)\s+RCDATA\s+(DISCARDABLE)?\s+\{.*?\}
+ ''', re.MULTILINE | re.VERBOSE | re.DOTALL)
+
+ def Parse(self):
+ '''Implementation for resource types w/braces (not BEGIN/END)
+ '''
+ rc_text = self._LoadInputFile()
+
+ out = ''
+ begin_count = 0
+ openbrace_count = 0
+ assert self.extkey
+ first_line_re = re.compile(r'\s*' + self.extkey + r'\b')
+ for line in rc_text.splitlines(True):
+ if out or first_line_re.match(line):
+ out += line
+
+ # We stop once the braces balance (could happen in one line).
+ begin_count_was = begin_count
+ if len(out) > 0:
+ openbrace_count += line.count('{')
+ begin_count += line.count('{')
+ begin_count -= line.count('}')
+ if ((begin_count_was == 1 and begin_count == 0) or
+ (openbrace_count > 0 and begin_count == 0)):
+ break
+
+ if len(out) == 0:
+ raise exception.SectionNotFound('%s in file %s' % (self.extkey, self.rc_file))
+
+ self.text_ = out
+
+ self._RegExpParse(self.dialog_re_, out)
+
+
+class Accelerators(Section):
+ '''An ACCELERATORS table.
+ '''
+
+ # A typical ACCELERATORS section looks like this:
+ #
+ # IDR_ACCELERATOR1 ACCELERATORS
+ # BEGIN
+ # "^C", ID_ACCELERATOR32770, ASCII, NOINVERT
+ # "^V", ID_ACCELERATOR32771, ASCII, NOINVERT
+ # VK_INSERT, ID_ACCELERATOR32772, VIRTKEY, CONTROL, NOINVERT
+ # END
+
+ accelerators_re_ = lazy_re.compile(r'''
+ # Match the ID on the first line
+ ^(?P<id1>[A-Z0-9_]+)\s+ACCELERATORS\s+
+ |
+ # Match accelerators specified as VK_XXX
+ \s+VK_[A-Z0-9_]+,\s*(?P<id2>[A-Z0-9_]+)\s*,
+ |
+ # Match accelerators specified as e.g. "^C"
+ \s+"[^"]*",\s+(?P<id3>[A-Z0-9_]+)\s*,
+ ''', re.MULTILINE | re.VERBOSE)
+
+ def Parse(self):
+ '''Knows how to parse ACCELERATORS resource sections.'''
+ self.ReadSection()
+ self._RegExpParse(self.accelerators_re_, self.text_)
diff --git a/tools/grit/grit/gather/rc_unittest.py b/tools/grit/grit/gather/rc_unittest.py
new file mode 100644
index 0000000000..3c26a4342a
--- /dev/null
+++ b/tools/grit/grit/gather/rc_unittest.py
@@ -0,0 +1,372 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.rc'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import rc
+from grit import util
+
+
+class RcUnittest(unittest.TestCase):
+
+ part_we_want = '''IDC_KLONKACC ACCELERATORS
+BEGIN
+ "?", IDM_ABOUT, ASCII, ALT
+ "/", IDM_ABOUT, ASCII, ALT
+END'''
+
+ def testSectionFromFile(self):
+ buf = '''IDC_SOMETHINGELSE BINGO
+BEGIN
+ BLA BLA
+ BLA BLA
+END
+%s
+
+IDC_KLONK BINGOBONGO
+BEGIN
+ HONGO KONGO
+END
+''' % self.part_we_want
+
+ f = StringIO(buf)
+
+ out = rc.Section(f, 'IDC_KLONKACC')
+ out.ReadSection()
+ self.failUnless(out.GetText() == self.part_we_want)
+
+ out = rc.Section(util.PathFromRoot(r'grit/testdata/klonk.rc'),
+ 'IDC_KLONKACC',
+ encoding='utf-16')
+ out.ReadSection()
+ out_text = out.GetText().replace('\t', '')
+ out_text = out_text.replace(' ', '')
+ self.part_we_want = self.part_we_want.replace(' ', '')
+ self.failUnless(out_text.strip() == self.part_we_want.strip())
+
+
+ def testDialog(self):
+ dlg = rc.Dialog(StringIO('''IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+ ICON IDI_KLONK,IDC_MYICON,14,9,20,20
+ LTEXT "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+ SS_NOPREFIX
+ LTEXT "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+ DEFPUSHBUTTON "OK",IDOK,195,6,30,11,WS_GROUP
+ CONTROL "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+ BS_AUTORADIOBUTTON,46,51,84,10
+ // try a line where the ID is on the continuation line
+ LTEXT "blablablabla blablabla blablablablablablablabla blablabla",
+ ID_SMURF, whatever...
+END
+'''), 'IDD_ABOUTBOX')
+ dlg.Parse()
+ self.failUnless(len(dlg.GetTextualIds()) == 7)
+ self.failUnless(len(dlg.GetCliques()) == 6)
+ self.failUnless(dlg.GetCliques()[1].GetMessage().GetRealContent() ==
+ 'klonk Version "yibbee" 1.0')
+
+ transl = dlg.Translate('en')
+ self.failUnless(transl.strip() == dlg.GetText().strip())
+
+ def testAlternateSkeleton(self):
+ dlg = rc.Dialog(StringIO('''IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+ LTEXT "Yipee skippy",IDC_STATIC,49,10,119,8,
+ SS_NOPREFIX
+END
+'''), 'IDD_ABOUTBOX')
+ dlg.Parse()
+
+ alt_dlg = rc.Dialog(StringIO('''IDD_ABOUTBOX DIALOGEX 040704, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "XXXXXXXXX"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+ LTEXT "XXXXXXXXXXXXXXXXX",IDC_STATIC,110978,10,119,8,
+ SS_NOPREFIX
+END
+'''), 'IDD_ABOUTBOX')
+ alt_dlg.Parse()
+
+ transl = dlg.Translate('en', skeleton_gatherer=alt_dlg)
+ self.failUnless(transl.count('040704') and
+ transl.count('110978'))
+ self.failUnless(transl.count('Yipee skippy'))
+
+ def testMenu(self):
+ menu = rc.Menu(StringIO('''IDC_KLONK MENU
+BEGIN
+ POPUP "&File """
+ BEGIN
+ MENUITEM "E&xit", IDM_EXIT
+ MENUITEM "This be ""Klonk"" me like", ID_FILE_THISBE
+ POPUP "gonk"
+ BEGIN
+ MENUITEM "Klonk && is ""good""", ID_GONK_KLONKIS
+ END
+ MENUITEM "This is a very long menu caption to try to see if we can make the ID go to a continuation line, blablabla blablabla bla blabla blablabla blablabla blablabla blablabla...",
+ ID_FILE_THISISAVERYLONGMENUCAPTIONTOTRYTOSEEIFWECANMAKETHEIDGOTOACONTINUATIONLINE
+ END
+ POPUP "&Help"
+ BEGIN
+ MENUITEM "&About ...", IDM_ABOUT
+ END
+END'''), 'IDC_KLONK')
+
+ menu.Parse()
+ self.failUnless(len(menu.GetTextualIds()) == 6)
+ self.failUnless(len(menu.GetCliques()) == 1)
+ self.failUnless(len(menu.GetCliques()[0].GetMessage().GetPlaceholders()) ==
+ 9)
+
+ transl = menu.Translate('en')
+ self.failUnless(transl.strip() == menu.GetText().strip())
+
+ def testVersion(self):
+ version = rc.Version(StringIO('''
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,0,0,1
+ PRODUCTVERSION 1,0,0,1
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x4L
+ FILETYPE 0x2L
+ FILESUBTYPE 0x0L
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904e4"
+ BEGIN
+ VALUE "CompanyName", "TODO: <Company name>"
+ VALUE "FileDescription", "TODO: <File description>"
+ VALUE "FileVersion", "1.0.0.1"
+ VALUE "LegalCopyright", "TODO: (c) <Company name>. All rights reserved."
+ VALUE "InternalName", "res_format_test.dll"
+ VALUE "OriginalFilename", "res_format_test.dll"
+ VALUE "ProductName", "TODO: <Product name>"
+ VALUE "ProductVersion", "1.0.0.1"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END
+'''.strip()), 'VS_VERSION_INFO')
+ version.Parse()
+ self.failUnless(len(version.GetTextualIds()) == 1)
+ self.failUnless(len(version.GetCliques()) == 4)
+
+ transl = version.Translate('en')
+ self.failUnless(transl.strip() == version.GetText().strip())
+
+
+ def testRegressionDialogBox(self):
+ dialog = rc.Dialog(StringIO('''
+IDD_SIDEBAR_WEATHER_PANEL_PROPPAGE DIALOGEX 0, 0, 205, 157
+STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ EDITTEXT IDC_SIDEBAR_WEATHER_NEW_CITY,3,27,112,14,ES_AUTOHSCROLL
+ DEFPUSHBUTTON "Add Location",IDC_SIDEBAR_WEATHER_ADD,119,27,50,14
+ LISTBOX IDC_SIDEBAR_WEATHER_CURR_CITIES,3,48,127,89,
+ LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
+ PUSHBUTTON "Move Up",IDC_SIDEBAR_WEATHER_MOVE_UP,134,104,50,14
+ PUSHBUTTON "Move Down",IDC_SIDEBAR_WEATHER_MOVE_DOWN,134,121,50,14
+ PUSHBUTTON "Remove",IDC_SIDEBAR_WEATHER_DELETE,134,48,50,14
+ LTEXT "To see current weather conditions and forecasts in the USA, enter the zip code (example: 94043) or city and state (example: Mountain View, CA).",
+ IDC_STATIC,3,0,199,25
+ CONTROL "Fahrenheit",IDC_SIDEBAR_WEATHER_FAHRENHEIT,"Button",
+ BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,3,144,51,10
+ CONTROL "Celsius",IDC_SIDEBAR_WEATHER_CELSIUS,"Button",
+ BS_AUTORADIOBUTTON,57,144,38,10
+END'''.strip()), 'IDD_SIDEBAR_WEATHER_PANEL_PROPPAGE')
+ dialog.Parse()
+ self.failUnless(len(dialog.GetTextualIds()) == 10)
+
+
+ def testRegressionDialogBox2(self):
+ dialog = rc.Dialog(StringIO('''
+IDD_SIDEBAR_EMAIL_PANEL_PROPPAGE DIALOG DISCARDABLE 0, 0, 264, 220
+STYLE WS_CHILD
+FONT 8, "MS Shell Dlg"
+BEGIN
+ GROUPBOX "Email Filters",IDC_STATIC,7,3,250,190
+ LTEXT "Click Add Filter to create the email filter.",IDC_STATIC,16,41,130,9
+ PUSHBUTTON "Add Filter...",IDC_SIDEBAR_EMAIL_ADD_FILTER,196,38,50,14
+ PUSHBUTTON "Remove",IDC_SIDEBAR_EMAIL_REMOVE,196,174,50,14
+ PUSHBUTTON "", IDC_SIDEBAR_EMAIL_HIDDEN, 200, 178, 5, 5, NOT WS_VISIBLE
+ LISTBOX IDC_SIDEBAR_EMAIL_LIST,16,60,230,108,
+ LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
+ LTEXT "You can prevent certain emails from showing up in the sidebar with a filter.",
+ IDC_STATIC,16,18,234,18
+END'''.strip()), 'IDD_SIDEBAR_EMAIL_PANEL_PROPPAGE')
+ dialog.Parse()
+ self.failUnless('IDC_SIDEBAR_EMAIL_HIDDEN' in dialog.GetTextualIds())
+
+
+ def testRegressionMenuId(self):
+ menu = rc.Menu(StringIO('''
+IDR_HYPERMENU_FOLDER MENU
+BEGIN
+ POPUP "HyperFolder"
+ BEGIN
+ MENUITEM "Open Containing Folder", IDM_OPENFOLDER
+ END
+END'''.strip()), 'IDR_HYPERMENU_FOLDER')
+ menu.Parse()
+ self.failUnless(len(menu.GetTextualIds()) == 2)
+
+ def testRegressionNewlines(self):
+ menu = rc.Menu(StringIO('''
+IDR_HYPERMENU_FOLDER MENU
+BEGIN
+ POPUP "Hyper\\nFolder"
+ BEGIN
+ MENUITEM "Open Containing Folder", IDM_OPENFOLDER
+ END
+END'''.strip()), 'IDR_HYPERMENU_FOLDER')
+ menu.Parse()
+ transl = menu.Translate('en')
+ # Shouldn't find \\n (the \n shouldn't be changed to \\n)
+ self.failUnless(transl.find('\\\\n') == -1)
+
+ def testRegressionTabs(self):
+ menu = rc.Menu(StringIO('''
+IDR_HYPERMENU_FOLDER MENU
+BEGIN
+ POPUP "Hyper\\tFolder"
+ BEGIN
+ MENUITEM "Open Containing Folder", IDM_OPENFOLDER
+ END
+END'''.strip()), 'IDR_HYPERMENU_FOLDER')
+ menu.Parse()
+ transl = menu.Translate('en')
+ # Shouldn't find \\t (the \t shouldn't be changed to \\t)
+ self.failUnless(transl.find('\\\\t') == -1)
+
+ def testEscapeUnescape(self):
+ original = 'Hello "bingo"\n How\\are\\you\\n?'
+ escaped = rc.Section.Escape(original)
+ self.failUnless(escaped == 'Hello ""bingo""\\n How\\\\are\\\\you\\\\n?')
+ unescaped = rc.Section.UnEscape(escaped)
+ self.failUnless(unescaped == original)
+
+ def testRegressionPathsWithSlashN(self):
+ original = '..\\\\..\\\\trs\\\\res\\\\nav_first.gif'
+ unescaped = rc.Section.UnEscape(original)
+ self.failUnless(unescaped == '..\\..\\trs\\res\\nav_first.gif')
+
+ def testRegressionDialogItemsTextOnly(self):
+ dialog = rc.Dialog(StringIO('''IDD_OPTIONS_SEARCH DIALOGEX 0, 0, 280, 292
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP |
+ WS_DISABLED | WS_CAPTION | WS_SYSMENU
+CAPTION "Search"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ GROUPBOX "Select search buttons and options",-1,7,5,266,262
+ CONTROL "",IDC_OPTIONS,"SysTreeView32",TVS_DISABLEDRAGDROP |
+ WS_BORDER | WS_TABSTOP | 0x800,16,19,248,218
+ LTEXT "Use Google site:",-1,26,248,52,8
+ COMBOBOX IDC_GOOGLE_HOME,87,245,177,256,CBS_DROPDOWNLIST |
+ WS_VSCROLL | WS_TABSTOP
+ PUSHBUTTON "Restore Defaults...",IDC_RESET,187,272,86,14
+END'''), 'IDD_OPTIONS_SEARCH')
+ dialog.Parse()
+ translateables = [c.GetMessage().GetRealContent()
+ for c in dialog.GetCliques()]
+ self.failUnless('Select search buttons and options' in translateables)
+ self.failUnless('Use Google site:' in translateables)
+
+ def testAccelerators(self):
+ acc = rc.Accelerators(StringIO('''\
+IDR_ACCELERATOR1 ACCELERATORS
+BEGIN
+ "^C", ID_ACCELERATOR32770, ASCII, NOINVERT
+ "^V", ID_ACCELERATOR32771, ASCII, NOINVERT
+ VK_INSERT, ID_ACCELERATOR32772, VIRTKEY, CONTROL, NOINVERT
+END
+'''), 'IDR_ACCELERATOR1')
+ acc.Parse()
+ self.failUnless(len(acc.GetTextualIds()) == 4)
+ self.failUnless(len(acc.GetCliques()) == 0)
+
+ transl = acc.Translate('en')
+ self.failUnless(transl.strip() == acc.GetText().strip())
+
+
+ def testRegressionEmptyString(self):
+ dlg = rc.Dialog(StringIO('''\
+IDD_CONFIRM_QUIT_GD_DLG DIALOGEX 0, 0, 267, 108
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP |
+ WS_CAPTION
+EXSTYLE WS_EX_TOPMOST
+CAPTION "Google Desktop"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ DEFPUSHBUTTON "&Yes",IDYES,82,87,50,14
+ PUSHBUTTON "&No",IDNO,136,87,50,14
+ ICON 32514,IDC_STATIC,7,9,21,20
+ EDITTEXT IDC_TEXTBOX,34,7,231,60,ES_MULTILINE | ES_READONLY | NOT WS_BORDER
+ CONTROL "",
+ IDC_ENABLE_GD_AUTOSTART,"Button",BS_AUTOCHECKBOX |
+ WS_TABSTOP,33,70,231,10
+END'''), 'IDD_CONFIRM_QUIT_GD_DLG')
+ dlg.Parse()
+
+ def Check():
+ self.failUnless(transl.count('IDC_ENABLE_GD_AUTOSTART'))
+ self.failUnless(transl.count('END'))
+
+ transl = dlg.Translate('de', pseudo_if_not_available=True,
+ fallback_to_english=True)
+ Check()
+ transl = dlg.Translate('de', pseudo_if_not_available=True,
+ fallback_to_english=False)
+ Check()
+ transl = dlg.Translate('de', pseudo_if_not_available=False,
+ fallback_to_english=True)
+ Check()
+ transl = dlg.Translate('de', pseudo_if_not_available=False,
+ fallback_to_english=False)
+ Check()
+ transl = dlg.Translate('en', pseudo_if_not_available=True,
+ fallback_to_english=True)
+ Check()
+ transl = dlg.Translate('en', pseudo_if_not_available=True,
+ fallback_to_english=False)
+ Check()
+ transl = dlg.Translate('en', pseudo_if_not_available=False,
+ fallback_to_english=True)
+ Check()
+ transl = dlg.Translate('en', pseudo_if_not_available=False,
+ fallback_to_english=False)
+ Check()
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/gather/regexp.py b/tools/grit/grit/gather/regexp.py
new file mode 100644
index 0000000000..97ce2cfbf7
--- /dev/null
+++ b/tools/grit/grit/gather/regexp.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A baseclass for simple gatherers based on regular expressions.
+'''
+
+from __future__ import print_function
+
+from grit.gather import skeleton_gatherer
+
+
+class RegexpGatherer(skeleton_gatherer.SkeletonGatherer):
+ '''Common functionality of gatherers based on parsing using a single
+ regular expression.
+ '''
+
+ DescriptionMapping_ = {
+ 'CAPTION' : 'This is a caption for a dialog',
+ 'CHECKBOX' : 'This is a label for a checkbox',
+ 'CONTROL': 'This is the text on a control',
+ 'CTEXT': 'This is a label for a control',
+ 'DEFPUSHBUTTON': 'This is a button definition',
+ 'GROUPBOX': 'This is a label for a grouping',
+ 'ICON': 'This is a label for an icon',
+ 'LTEXT': 'This is the text for a label',
+ 'PUSHBUTTON': 'This is the text for a button',
+ }
+
+ # Contextualization elements. Used for adding additional information
+ # to the message bundle description string from RC files.
+ def AddDescriptionElement(self, string):
+ if string in self.DescriptionMapping_:
+ description = self.DescriptionMapping_[string]
+ else:
+ description = string
+ if self.single_message_:
+ self.single_message_.SetDescription(description)
+ else:
+ if (self.translatable_chunk_):
+ message = self.skeleton_[len(self.skeleton_) - 1].GetMessage()
+ message.SetDescription(description)
+
+ def _RegExpParse(self, regexp, text_to_parse):
+ '''An implementation of Parse() that can be used for resource sections that
+ can be parsed using a single multi-line regular expression.
+
+ All translateables must be in named groups that have names starting with
+ 'text'. All textual IDs must be in named groups that have names starting
+ with 'id'. All type definitions that can be included in the description
+ field for contextualization purposes should have a name that starts with
+ 'type'.
+
+ Args:
+ regexp: re.compile('...', re.MULTILINE)
+ text_to_parse:
+ '''
+ chunk_start = 0
+ for match in regexp.finditer(text_to_parse):
+ groups = match.groupdict()
+ keys = sorted(groups.keys())
+ self.translatable_chunk_ = False
+ for group in keys:
+ if group.startswith('id') and groups[group]:
+ self._AddTextualId(groups[group])
+ elif group.startswith('text') and groups[group]:
+ self._AddNontranslateableChunk(
+ text_to_parse[chunk_start : match.start(group)])
+ chunk_start = match.end(group) # Next chunk will start after the match
+ self._AddTranslateableChunk(groups[group])
+ elif group.startswith('type') and groups[group]:
+ # Add the description to the skeleton_ list. This works because
+ # we are using a sort set of keys, and because we assume that the
+ # group name used for descriptions (type) will come after the "text"
+ # group in alphabetical order. We also assume that there cannot be
+ # more than one description per regular expression match.
+ self.AddDescriptionElement(groups[group])
+
+ self._AddNontranslateableChunk(text_to_parse[chunk_start:])
+
+ if self.single_message_:
+ self.skeleton_.append(self.uberclique.MakeClique(self.single_message_))
diff --git a/tools/grit/grit/gather/skeleton_gatherer.py b/tools/grit/grit/gather/skeleton_gatherer.py
new file mode 100644
index 0000000000..b11862b314
--- /dev/null
+++ b/tools/grit/grit/gather/skeleton_gatherer.py
@@ -0,0 +1,149 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A baseclass for simple gatherers that store their gathered resource in a
+list.
+'''
+
+from __future__ import print_function
+
+import six
+
+from grit.gather import interface
+from grit import clique
+from grit import exception
+from grit import tclib
+
+
+class SkeletonGatherer(interface.GathererBase):
+ '''Common functionality of gatherers that parse their input as a skeleton of
+ translatable and nontranslatable chunks.
+ '''
+
+ def __init__(self, *args, **kwargs):
+ super(SkeletonGatherer, self).__init__(*args, **kwargs)
+ # List of parts of the document. Translateable parts are
+ # clique.MessageClique objects, nontranslateable parts are plain strings.
+ # Translated messages are inserted back into the skeleton using the quoting
+ # rules defined by self.Escape()
+ self.skeleton_ = []
+ # A list of the names of IDs that need to be defined for this resource
+ # section to compile correctly.
+ self.ids_ = []
+ # True if Parse() has already been called.
+ self.have_parsed_ = False
+ # True if a translatable chunk has been added
+ self.translatable_chunk_ = False
+ # If not None, all parts of the document will be put into this single
+ # message; otherwise the normal skeleton approach is used.
+ self.single_message_ = None
+ # Number to use for the next placeholder name. Used only if single_message
+ # is not None
+ self.ph_counter_ = 1
+
+ def GetText(self):
+ '''Returns the original text of the section'''
+ return self.text_
+
+ def Escape(self, text):
+ '''Subclasses can override. Base impl is identity.
+ '''
+ return text
+
+ def UnEscape(self, text):
+ '''Subclasses can override. Base impl is identity.
+ '''
+ return text
+
+ def GetTextualIds(self):
+ '''Returns the list of textual IDs that need to be defined for this
+ resource section to compile correctly.'''
+ return self.ids_
+
+ def _AddTextualId(self, id):
+ self.ids_.append(id)
+
+ def GetCliques(self):
+ '''Returns the message cliques for each translateable message in the
+ resource section.'''
+ return [x for x in self.skeleton_ if isinstance(x, clique.MessageClique)]
+
+ def Translate(self, lang, pseudo_if_not_available=True,
+ skeleton_gatherer=None, fallback_to_english=False):
+ if len(self.skeleton_) == 0:
+ raise exception.NotReady()
+ if skeleton_gatherer:
+ assert len(skeleton_gatherer.skeleton_) == len(self.skeleton_)
+
+ out = []
+ for ix in range(len(self.skeleton_)):
+ if isinstance(self.skeleton_[ix], six.string_types):
+ if skeleton_gatherer:
+ # Make sure the skeleton is like the original
+ assert(isinstance(skeleton_gatherer.skeleton_[ix], six.string_types))
+ out.append(skeleton_gatherer.skeleton_[ix])
+ else:
+ out.append(self.skeleton_[ix])
+ else:
+ if skeleton_gatherer: # Make sure the skeleton is like the original
+ assert(not isinstance(skeleton_gatherer.skeleton_[ix],
+ six.string_types))
+ msg = self.skeleton_[ix].MessageForLanguage(lang,
+ pseudo_if_not_available,
+ fallback_to_english)
+
+ def MyEscape(text):
+ return self.Escape(text)
+ text = msg.GetRealContent(escaping_function=MyEscape)
+ out.append(text)
+ return ''.join(out)
+
+ def Parse(self):
+ '''Parses the section. Implemented by subclasses. Idempotent.'''
+ raise NotImplementedError()
+
+ def _AddNontranslateableChunk(self, chunk):
+ '''Adds a nontranslateable chunk.'''
+ if self.single_message_:
+ ph = tclib.Placeholder('XX%02dXX' % self.ph_counter_, chunk, chunk)
+ self.ph_counter_ += 1
+ self.single_message_.AppendPlaceholder(ph)
+ else:
+ self.skeleton_.append(chunk)
+
+ def _AddTranslateableChunk(self, chunk):
+ '''Adds a translateable chunk. It will be unescaped before being added.'''
+ # We don't want empty messages since they are redundant and the TC
+ # doesn't allow them.
+ if chunk == '':
+ return
+
+ unescaped_text = self.UnEscape(chunk)
+ if self.single_message_:
+ self.single_message_.AppendText(unescaped_text)
+ else:
+ self.skeleton_.append(self.uberclique.MakeClique(
+ tclib.Message(text=unescaped_text)))
+ self.translatable_chunk_ = True
+
+ def SubstituteMessages(self, substituter):
+ '''Applies substitutions to all messages in the tree.
+
+ Goes through the skeleton and finds all MessageCliques.
+
+ Args:
+ substituter: a grit.util.Substituter object.
+ '''
+ if self.single_message_:
+ self.single_message_ = substituter.SubstituteMessage(self.single_message_)
+ new_skel = []
+ for chunk in self.skeleton_:
+ if isinstance(chunk, clique.MessageClique):
+ old_message = chunk.GetMessage()
+ new_message = substituter.SubstituteMessage(old_message)
+ if new_message is not old_message:
+ new_skel.append(self.uberclique.MakeClique(new_message))
+ continue
+ new_skel.append(chunk)
+ self.skeleton_ = new_skel
diff --git a/tools/grit/grit/gather/tr_html.py b/tools/grit/grit/gather/tr_html.py
new file mode 100644
index 0000000000..60a9bfaf4e
--- /dev/null
+++ b/tools/grit/grit/gather/tr_html.py
@@ -0,0 +1,743 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A gatherer for the TotalRecall brand of HTML templates with replaceable
+portions. We wanted to reuse extern.tclib.api.handlers.html.TCHTMLParser
+but this proved impossible due to the fact that the TotalRecall HTML templates
+are in general quite far from parseable HTML and the TCHTMLParser derives
+
+from HTMLParser.HTMLParser which requires relatively well-formed HTML. Some
+examples of "HTML" from the TotalRecall HTML templates that wouldn't be
+parseable include things like:
+
+ <a [PARAMS]>blabla</a> (not parseable because attributes are invalid)
+
+ <table><tr><td>[LOTSOFSTUFF]</tr></table> (not parseable because closing
+ </td> is in the HTML [LOTSOFSTUFF]
+ is replaced by)
+
+The other problem with using general parsers (such as TCHTMLParser) is that
+we want to make sure we output the TotalRecall template with as little changes
+as possible in terms of whitespace characters, layout etc. With any parser
+that generates a parse tree, and generates output by dumping the parse tree,
+we would always have little inconsistencies which could cause bugs (the
+TotalRecall template stuff is quite brittle and can break if e.g. a tab
+character is replaced with spaces).
+
+The solution, which may be applicable to some other HTML-like template
+languages floating around Google, is to create a parser with a simple state
+machine that keeps track of what kind of tag it's inside, and whether it's in
+a translateable section or not. Translateable sections are:
+
+a) text (including [BINGO] replaceables) inside of tags that
+ can contain translateable text (which is all tags except
+ for a few)
+
+b) text inside of an 'alt' attribute in an <image> element, or
+ the 'value' attribute of a <submit>, <button> or <text>
+ element.
+
+The parser does not build up a parse tree but rather a "skeleton" which
+is a list of nontranslateable strings intermingled with grit.clique.MessageClique
+objects. This simplifies the parser considerably compared to a regular HTML
+parser. To output a translated document, each item in the skeleton is
+printed out, with the relevant Translation from each MessageCliques being used
+for the requested language.
+
+This implementation borrows some code, constants and ideas from
+extern.tclib.api.handlers.html.TCHTMLParser.
+'''
+
+from __future__ import print_function
+
+import re
+
+import six
+
+from grit import clique
+from grit import exception
+from grit import lazy_re
+from grit import util
+from grit import tclib
+
+from grit.gather import interface
+
+
+# HTML tags which break (separate) chunks.
+_BLOCK_TAGS = ['script', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'br',
+ 'body', 'style', 'head', 'title', 'table', 'tr', 'td', 'th',
+ 'ul', 'ol', 'dl', 'nl', 'li', 'div', 'object', 'center',
+ 'html', 'link', 'form', 'select', 'textarea',
+ 'button', 'option', 'map', 'area', 'blockquote', 'pre',
+ 'meta', 'xmp', 'noscript', 'label', 'tbody', 'thead',
+ 'script', 'style', 'pre', 'iframe', 'img', 'input', 'nowrap',
+ 'fieldset', 'legend']
+
+# HTML tags which may appear within a chunk.
+_INLINE_TAGS = ['b', 'i', 'u', 'tt', 'code', 'font', 'a', 'span', 'small',
+ 'key', 'nobr', 'url', 'em', 's', 'sup', 'strike',
+ 'strong']
+
+# HTML tags within which linebreaks are significant.
+_PREFORMATTED_TAGS = ['textarea', 'xmp', 'pre']
+
+# An array mapping some of the inline HTML tags to more meaningful
+# names for those tags. This will be used when generating placeholders
+# representing these tags.
+_HTML_PLACEHOLDER_NAMES = { 'a' : 'link', 'br' : 'break', 'b' : 'bold',
+ 'i' : 'italic', 'li' : 'item', 'ol' : 'ordered_list', 'p' : 'paragraph',
+ 'ul' : 'unordered_list', 'img' : 'image', 'em' : 'emphasis' }
+
+# We append each of these characters in sequence to distinguish between
+# different placeholders with basically the same name (e.g. BOLD1, BOLD2).
+# Keep in mind that a placeholder name must not be a substring of any other
+# placeholder name in the same message, so we can't simply count (BOLD_1
+# would be a substring of BOLD_10).
+_SUFFIXES = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+
+# Matches whitespace in an HTML document. Also matches HTML comments, which are
+# treated as whitespace.
+_WHITESPACE = lazy_re.compile(r'(\s| |\\n|\\r|<!--\s*desc\s*=.*?-->)+',
+ re.DOTALL)
+
+# Matches whitespace sequences which can be folded into a single whitespace
+# character. This matches single characters so that non-spaces are replaced
+# with spaces.
+_FOLD_WHITESPACE = lazy_re.compile(r'\s+')
+
+# Finds a non-whitespace character
+_NON_WHITESPACE = lazy_re.compile(r'\S')
+
+# Matches two or more in a row (a single   is not changed into
+# placeholders because different languages require different numbers of spaces
+# and placeholders must match exactly; more than one is probably a "special"
+# whitespace sequence and should be turned into a placeholder).
+_NBSP = lazy_re.compile(r' ( )+')
+
+# Matches nontranslateable chunks of the document
+_NONTRANSLATEABLES = lazy_re.compile(r'''
+ <\s*script.+?<\s*/\s*script\s*>
+ |
+ <\s*style.+?<\s*/\s*style\s*>
+ |
+ <!--.+?-->
+ |
+ <\?IMPORT\s.+?> # import tag
+ |
+ <\s*[a-zA-Z_]+:.+?> # custom tag (open)
+ |
+ <\s*/\s*[a-zA-Z_]+:.+?> # custom tag (close)
+ |
+ <!\s*[A-Z]+\s*([^>]+|"[^"]+"|'[^']+')*?>
+ ''', re.MULTILINE | re.DOTALL | re.VERBOSE | re.IGNORECASE)
+
+# Matches a tag and its attributes
+_ELEMENT = lazy_re.compile(r'''
+ # Optional closing /, element name
+ <\s*(?P<closing>/)?\s*(?P<element>[a-zA-Z0-9]+)\s*
+ # Attributes and/or replaceables inside the tag, if any
+ (?P<atts>(
+ \s*([a-zA-Z_][-:.a-zA-Z_0-9]*) # Attribute name
+ (\s*=\s*(\'[^\']*\'|"[^"]*"|[-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?
+ |
+ \s*\[(\$?\~)?([A-Z0-9-_]+?)(\~\$?)?\]
+ )*)
+ \s*(?P<empty>/)?\s*> # Optional empty-tag closing /, and tag close
+ ''',
+ re.MULTILINE | re.DOTALL | re.VERBOSE)
+
+# Matches elements that may have translateable attributes. The value of these
+# special attributes is given by group 'value1' or 'value2'. Note that this
+# regexp demands that the attribute value be quoted; this is necessary because
+# the non-tree-building nature of the parser means we don't know when we're
+# writing out attributes, so we wouldn't know to escape spaces.
+_SPECIAL_ELEMENT = lazy_re.compile(r'''
+ <\s*(
+ input[^>]+?value\s*=\s*(\'(?P<value3>[^\']*)\'|"(?P<value4>[^"]*)")
+ [^>]+type\s*=\s*"?'?(button|reset|text|submit)'?"?
+ |
+ (
+ table[^>]+?title\s*=
+ |
+ img[^>]+?alt\s*=
+ |
+ input[^>]+?type\s*=\s*"?'?(button|reset|text|submit)'?"?[^>]+?value\s*=
+ )
+ \s*(\'(?P<value1>[^\']*)\'|"(?P<value2>[^"]*)")
+ )[^>]*?>
+ ''', re.MULTILINE | re.DOTALL | re.VERBOSE | re.IGNORECASE)
+
+# Matches stuff that is translateable if it occurs in the right context
+# (between tags). This includes all characters and character entities.
+# Note that this also matches which needs to be handled as whitespace
+# before this regexp is applied.
+_CHARACTERS = lazy_re.compile(r'''
+ (
+ \w
+ |
+ [\!\@\#\$\%\^\*\(\)\-\=\_\+\[\]\{\}\\\|\;\:\'\"\,\.\/\?\`\~]
+ |
+ &(\#[0-9]+|\#x[0-9a-fA-F]+|[A-Za-z0-9]+);
+ )+
+ ''', re.MULTILINE | re.DOTALL | re.VERBOSE)
+
+# Matches Total Recall's "replaceable" tags, which are just any text
+# in capitals enclosed by delimiters like [] or [~~] or [$~~$] (e.g. [HELLO],
+# [~HELLO~] and [$~HELLO~$]).
+_REPLACEABLE = lazy_re.compile(r'\[(\$?\~)?(?P<name>[A-Z0-9-_]+?)(\~\$?)?\]',
+ re.MULTILINE)
+
+
+# Matches the silly [!]-prefixed "header" that is used in some TotalRecall
+# templates.
+_SILLY_HEADER = lazy_re.compile(r'\[!\]\ntitle\t(?P<title>[^\n]+?)\n.+?\n\n',
+ re.MULTILINE | re.DOTALL)
+
+
+# Matches a comment that provides a description for the message it occurs in.
+_DESCRIPTION_COMMENT = lazy_re.compile(
+ r'<!--\s*desc\s*=\s*(?P<description>.+?)\s*-->', re.DOTALL)
+
+# Matches a comment which is used to break apart multiple messages.
+_MESSAGE_BREAK_COMMENT = lazy_re.compile(r'<!--\s*message-break\s*-->',
+ re.DOTALL)
+
+# Matches a comment which is used to prevent block tags from splitting a message
+_MESSAGE_NO_BREAK_COMMENT = re.compile(r'<!--\s*message-no-break\s*-->',
+ re.DOTALL)
+
+
+_DEBUG = 0
+def _DebugPrint(text):
+ if _DEBUG:
+ print(text.encode('utf-8'))
+
+
+class HtmlChunks(object):
+ '''A parser that knows how to break an HTML-like document into a list of
+ chunks, where each chunk is either translateable or non-translateable.
+ The chunks are unmodified sections of the original document, so concatenating
+ the text of all chunks would result in the original document.'''
+
+ def InTranslateable(self):
+ return self.last_translateable != -1
+
+ def Rest(self):
+ return self.text_[self.current:]
+
+ def StartTranslateable(self):
+ assert not self.InTranslateable()
+ if self.current != 0:
+ # Append a nontranslateable chunk
+ chunk_text = self.text_[self.chunk_start : self.last_nontranslateable + 1]
+ # Needed in the case where document starts with a translateable.
+ if len(chunk_text) > 0:
+ self.AddChunk(False, chunk_text)
+ self.chunk_start = self.last_nontranslateable + 1
+ self.last_translateable = self.current
+ self.last_nontranslateable = -1
+
+ def EndTranslateable(self):
+ assert self.InTranslateable()
+ # Append a translateable chunk
+ self.AddChunk(True,
+ self.text_[self.chunk_start : self.last_translateable + 1])
+ self.chunk_start = self.last_translateable + 1
+ self.last_translateable = -1
+ self.last_nontranslateable = self.current
+
+ def AdvancePast(self, match):
+ self.current += match.end()
+
+ def AddChunk(self, translateable, text):
+ '''Adds a chunk to self, removing linebreaks and duplicate whitespace
+ if appropriate.
+ '''
+ m = _DESCRIPTION_COMMENT.search(text)
+ if m:
+ self.last_description = m.group('description')
+ # Remove the description from the output text
+ text = _DESCRIPTION_COMMENT.sub('', text)
+
+ m = _MESSAGE_BREAK_COMMENT.search(text)
+ if m:
+ # Remove the coment from the output text. It should already effectively
+ # break apart messages.
+ text = _MESSAGE_BREAK_COMMENT.sub('', text)
+
+ if translateable and not self.last_element_ in _PREFORMATTED_TAGS:
+ if self.fold_whitespace_:
+ # Fold whitespace sequences if appropriate. This is optional because it
+ # alters the output strings.
+ text = _FOLD_WHITESPACE.sub(' ', text)
+ else:
+ text = text.replace('\n', ' ')
+ text = text.replace('\r', ' ')
+ # This whitespace folding doesn't work in all cases, thus the
+ # fold_whitespace flag to support backwards compatibility.
+ text = text.replace(' ', ' ')
+ text = text.replace(' ', ' ')
+
+ if translateable:
+ description = self.last_description
+ self.last_description = ''
+ else:
+ description = ''
+
+ if text != '':
+ self.chunks_.append((translateable, text, description))
+
+ def Parse(self, text, fold_whitespace):
+ '''Parses self.text_ into an intermediate format stored in self.chunks_
+ which is translateable and nontranslateable chunks. Also returns
+ self.chunks_
+
+ Args:
+ text: The HTML for parsing.
+ fold_whitespace: Whether whitespace sequences should be folded into a
+ single space.
+
+ Return:
+ [chunk1, chunk2, chunk3, ...] (instances of class Chunk)
+ '''
+ #
+ # Chunker state
+ #
+
+ self.text_ = text
+ self.fold_whitespace_ = fold_whitespace
+
+ # A list of tuples (is_translateable, text) which represents the document
+ # after chunking.
+ self.chunks_ = []
+
+ # Start index of the last chunk, whether translateable or not
+ self.chunk_start = 0
+
+ # Index of the last for-sure translateable character if we are parsing
+ # a translateable chunk, -1 to indicate we are not in a translateable chunk.
+ # This is needed so that we don't include trailing whitespace in the
+ # translateable chunk (whitespace is neutral).
+ self.last_translateable = -1
+
+ # Index of the last for-sure nontranslateable character if we are parsing
+ # a nontranslateable chunk, -1 if we are not in a nontranslateable chunk.
+ # This is needed to make sure we can group e.g. "<b>Hello</b> there"
+ # together instead of just "Hello</b> there" which would be much worse
+ # for translation.
+ self.last_nontranslateable = -1
+
+ # Index of the character we're currently looking at.
+ self.current = 0
+
+ # The name of the last block element parsed.
+ self.last_element_ = ''
+
+ # The last explicit description we found.
+ self.last_description = ''
+
+ # Whether no-break was the last chunk seen
+ self.last_nobreak = False
+
+ while self.current < len(self.text_):
+ _DebugPrint('REST: %s' % self.text_[self.current:self.current+60])
+
+ m = _MESSAGE_NO_BREAK_COMMENT.match(self.Rest())
+ if m:
+ self.AdvancePast(m)
+ self.last_nobreak = True
+ continue
+
+ # Try to match whitespace
+ m = _WHITESPACE.match(self.Rest())
+ if m:
+ # Whitespace is neutral, it just advances 'current' and does not switch
+ # between translateable/nontranslateable. If we are in a
+ # nontranslateable section that extends to the current point, we extend
+ # it to include the whitespace. If we are in a translateable section,
+ # we do not extend it until we find
+ # more translateable parts, because we never want a translateable chunk
+ # to end with whitespace.
+ if (not self.InTranslateable() and
+ self.last_nontranslateable == self.current - 1):
+ self.last_nontranslateable = self.current + m.end() - 1
+ self.AdvancePast(m)
+ continue
+
+ # Then we try to match nontranslateables
+ m = _NONTRANSLATEABLES.match(self.Rest())
+ if m:
+ if self.InTranslateable():
+ self.EndTranslateable()
+ self.last_nontranslateable = self.current + m.end() - 1
+ self.AdvancePast(m)
+ continue
+
+ # Now match all other HTML element tags (opening, closing, or empty, we
+ # don't care).
+ m = _ELEMENT.match(self.Rest())
+ if m:
+ element_name = m.group('element').lower()
+ if element_name in _BLOCK_TAGS:
+ self.last_element_ = element_name
+ if self.InTranslateable():
+ if self.last_nobreak:
+ self.last_nobreak = False
+ else:
+ self.EndTranslateable()
+
+ # Check for "special" elements, i.e. ones that have a translateable
+ # attribute, and handle them correctly. Note that all of the
+ # "special" elements are block tags, so no need to check for this
+ # if the tag is not a block tag.
+ sm = _SPECIAL_ELEMENT.match(self.Rest())
+ if sm:
+ # Get the appropriate group name
+ for group in sm.groupdict():
+ if sm.groupdict()[group]:
+ break
+
+ # First make a nontranslateable chunk up to and including the
+ # quote before the translateable attribute value
+ self.AddChunk(False, self.text_[
+ self.chunk_start : self.current + sm.start(group)])
+ # Then a translateable for the translateable bit
+ self.AddChunk(True, self.Rest()[sm.start(group) : sm.end(group)])
+ # Finally correct the data invariant for the parser
+ self.chunk_start = self.current + sm.end(group)
+
+ self.last_nontranslateable = self.current + m.end() - 1
+ elif self.InTranslateable():
+ # We're in a translateable and the tag is an inline tag, so we
+ # need to include it in the translateable.
+ self.last_translateable = self.current + m.end() - 1
+ self.AdvancePast(m)
+ continue
+
+ # Anything else we find must be translateable, so we advance one character
+ # at a time until one of the above matches.
+ if not self.InTranslateable():
+ self.StartTranslateable()
+ else:
+ self.last_translateable = self.current
+ self.current += 1
+
+ # Close the final chunk
+ if self.InTranslateable():
+ self.AddChunk(True, self.text_[self.chunk_start : ])
+ else:
+ self.AddChunk(False, self.text_[self.chunk_start : ])
+
+ return self.chunks_
+
+
+def HtmlToMessage(html, include_block_tags=False, description=''):
+ '''Takes a bit of HTML, which must contain only "inline" HTML elements,
+ and changes it into a tclib.Message. This involves escaping any entities and
+ replacing any HTML code with placeholders.
+
+ If include_block_tags is true, no error will be given if block tags (e.g.
+ <p> or <br>) are included in the HTML.
+
+ Args:
+ html: 'Hello <b>[USERNAME]</b>, how <i>are</i> you?'
+ include_block_tags: False
+
+ Return:
+ tclib.Message('Hello START_BOLD1USERNAMEEND_BOLD, '
+ 'howNBSPSTART_ITALICareEND_ITALIC you?',
+ [ Placeholder('START_BOLD', '<b>', ''),
+ Placeholder('USERNAME', '[USERNAME]', ''),
+ Placeholder('END_BOLD', '</b>', ''),
+ Placeholder('START_ITALIC', '<i>', ''),
+ Placeholder('END_ITALIC', '</i>', ''), ])
+ '''
+ # Approach is:
+ # - first placeholderize, finding <elements>, [REPLACEABLES] and
+ # - then escape all character entities in text in-between placeholders
+
+ parts = [] # List of strings (for text chunks) and tuples (ID, original)
+ # for placeholders
+
+ count_names = {} # Map of base names to number of times used
+ end_names = {} # Map of base names to stack of end tags (for correct nesting)
+
+ def MakeNameClosure(base, type = ''):
+ '''Returns a closure that can be called once all names have been allocated
+ to return the final name of the placeholder. This allows us to minimally
+ number placeholders for non-overlap.
+
+ Also ensures that END_XXX_Y placeholders have the same Y as the
+ corresponding BEGIN_XXX_Y placeholder when we have nested tags of the same
+ type.
+
+ Args:
+ base: 'phname'
+ type: '' | 'begin' | 'end'
+
+ Return:
+ Closure()
+ '''
+ name = base.upper()
+ if type != '':
+ name = ('%s_%s' % (type, base)).upper()
+
+ count_names.setdefault(name, 0)
+ count_names[name] += 1
+
+ def MakeFinalName(name_ = name, index = count_names[name] - 1):
+ if type.lower() == 'end' and end_names.get(base):
+ return end_names[base].pop(-1) # For correct nesting
+ if count_names[name_] != 1:
+ name_ = '%s_%s' % (name_, _SUFFIXES[index])
+ # We need to use a stack to ensure that the end-tag suffixes match
+ # the begin-tag suffixes. Only needed when more than one tag of the
+ # same type.
+ if type == 'begin':
+ end_name = ('END_%s_%s' % (base, _SUFFIXES[index])).upper()
+ if base in end_names:
+ end_names[base].append(end_name)
+ else:
+ end_names[base] = [end_name]
+
+ return name_
+
+ return MakeFinalName
+
+ current = 0
+ last_nobreak = False
+
+ while current < len(html):
+ m = _MESSAGE_NO_BREAK_COMMENT.match(html[current:])
+ if m:
+ last_nobreak = True
+ current += m.end()
+ continue
+
+ m = _NBSP.match(html[current:])
+ if m:
+ parts.append((MakeNameClosure('SPACE'), m.group()))
+ current += m.end()
+ continue
+
+ m = _REPLACEABLE.match(html[current:])
+ if m:
+ # Replaceables allow - but placeholders don't, so replace - with _
+ ph_name = MakeNameClosure('X_%s_X' % m.group('name').replace('-', '_'))
+ parts.append((ph_name, m.group()))
+ current += m.end()
+ continue
+
+ m = _SPECIAL_ELEMENT.match(html[current:])
+ if m:
+ if not include_block_tags:
+ if last_nobreak:
+ last_nobreak = False
+ else:
+ raise exception.BlockTagInTranslateableChunk(html)
+ element_name = 'block' # for simplification
+ # Get the appropriate group name
+ for group in m.groupdict():
+ if m.groupdict()[group]:
+ break
+ parts.append((MakeNameClosure(element_name, 'begin'),
+ html[current : current + m.start(group)]))
+ parts.append(m.group(group))
+ parts.append((MakeNameClosure(element_name, 'end'),
+ html[current + m.end(group) : current + m.end()]))
+ current += m.end()
+ continue
+
+ m = _ELEMENT.match(html[current:])
+ if m:
+ element_name = m.group('element').lower()
+ if not include_block_tags and not element_name in _INLINE_TAGS:
+ if last_nobreak:
+ last_nobreak = False
+ else:
+ raise exception.BlockTagInTranslateableChunk(html[current:])
+ if element_name in _HTML_PLACEHOLDER_NAMES: # use meaningful names
+ element_name = _HTML_PLACEHOLDER_NAMES[element_name]
+
+ # Make a name for the placeholder
+ type = ''
+ if not m.group('empty'):
+ if m.group('closing'):
+ type = 'end'
+ else:
+ type = 'begin'
+ parts.append((MakeNameClosure(element_name, type), m.group()))
+ current += m.end()
+ continue
+
+ if len(parts) and isinstance(parts[-1], six.string_types):
+ parts[-1] += html[current]
+ else:
+ parts.append(html[current])
+ current += 1
+
+ msg_text = ''
+ placeholders = []
+ for part in parts:
+ if isinstance(part, tuple):
+ final_name = part[0]()
+ original = part[1]
+ msg_text += final_name
+ placeholders.append(tclib.Placeholder(final_name, original, '(HTML code)'))
+ else:
+ msg_text += part
+
+ msg = tclib.Message(text=msg_text, placeholders=placeholders,
+ description=description)
+ content = msg.GetContent()
+ for ix in range(len(content)):
+ if isinstance(content[ix], six.string_types):
+ content[ix] = util.UnescapeHtml(content[ix], replace_nbsp=False)
+
+ return msg
+
+
+class TrHtml(interface.GathererBase):
+ '''Represents a document or message in the template format used by
+ Total Recall for HTML documents.'''
+
+ def __init__(self, *args, **kwargs):
+ super(TrHtml, self).__init__(*args, **kwargs)
+ self.have_parsed_ = False
+ self.skeleton_ = [] # list of strings and MessageClique objects
+ self.fold_whitespace_ = False
+
+ def SetAttributes(self, attrs):
+ '''Sets node attributes used by the gatherer.
+
+ This checks the fold_whitespace attribute.
+
+ Args:
+ attrs: The mapping of node attributes.
+ '''
+ self.fold_whitespace_ = ('fold_whitespace' in attrs and
+ attrs['fold_whitespace'] == 'true')
+
+ def GetText(self):
+ '''Returns the original text of the HTML document'''
+ return self.text_
+
+ def GetTextualIds(self):
+ return [self.extkey]
+
+ def GetCliques(self):
+ '''Returns the message cliques for each translateable message in the
+ document.'''
+ return [x for x in self.skeleton_ if isinstance(x, clique.MessageClique)]
+
+ def Translate(self, lang, pseudo_if_not_available=True,
+ skeleton_gatherer=None, fallback_to_english=False):
+ '''Returns this document with translateable messages filled with
+ the translation for language 'lang'.
+
+ Args:
+ lang: 'en'
+ pseudo_if_not_available: True
+
+ Return:
+ 'ID_THIS_SECTION TYPE\n...BEGIN\n "Translated message"\n......\nEND
+
+ Raises:
+ grit.exception.NotReady() if used before Parse() has been successfully
+ called.
+ grit.exception.NoSuchTranslation() if 'pseudo_if_not_available' is false
+ and there is no translation for the requested language.
+ '''
+ if len(self.skeleton_) == 0:
+ raise exception.NotReady()
+
+ # TODO(joi) Implement support for skeleton gatherers here.
+
+ out = []
+ for item in self.skeleton_:
+ if isinstance(item, six.string_types):
+ out.append(item)
+ else:
+ msg = item.MessageForLanguage(lang,
+ pseudo_if_not_available,
+ fallback_to_english)
+ for content in msg.GetContent():
+ if isinstance(content, tclib.Placeholder):
+ out.append(content.GetOriginal())
+ else:
+ # We escape " characters to increase the chance that attributes
+ # will be properly escaped.
+ out.append(util.EscapeHtml(content, True))
+
+ return ''.join(out)
+
+ def Parse(self):
+ if self.have_parsed_:
+ return
+ self.have_parsed_ = True
+
+ text = self._LoadInputFile()
+
+ # Ignore the BOM character if the document starts with one.
+ if text.startswith(u'\ufeff'):
+ text = text[1:]
+
+ self.text_ = text
+
+ # Parsing is done in two phases: First, we break the document into
+ # translateable and nontranslateable chunks. Second, we run through each
+ # translateable chunk and insert placeholders for any HTML elements,
+ # unescape escaped characters, etc.
+
+ # First handle the silly little [!]-prefixed header because it's not
+ # handled by our HTML parsers.
+ m = _SILLY_HEADER.match(text)
+ if m:
+ self.skeleton_.append(text[:m.start('title')])
+ self.skeleton_.append(self.uberclique.MakeClique(
+ tclib.Message(text=text[m.start('title'):m.end('title')])))
+ self.skeleton_.append(text[m.end('title') : m.end()])
+ text = text[m.end():]
+
+ chunks = HtmlChunks().Parse(text, self.fold_whitespace_)
+
+ for chunk in chunks:
+ if chunk[0]: # Chunk is translateable
+ self.skeleton_.append(self.uberclique.MakeClique(
+ HtmlToMessage(chunk[1], description=chunk[2])))
+ else:
+ self.skeleton_.append(chunk[1])
+
+ # Go through the skeleton and change any messages that consist solely of
+ # placeholders and whitespace into nontranslateable strings.
+ for ix in range(len(self.skeleton_)):
+ got_text = False
+ if isinstance(self.skeleton_[ix], clique.MessageClique):
+ msg = self.skeleton_[ix].GetMessage()
+ for item in msg.GetContent():
+ if (isinstance(item, six.string_types)
+ and _NON_WHITESPACE.search(item) and item != ' '):
+ got_text = True
+ break
+ if not got_text:
+ self.skeleton_[ix] = msg.GetRealContent()
+
+ def SubstituteMessages(self, substituter):
+ '''Applies substitutions to all messages in the tree.
+
+ Goes through the skeleton and finds all MessageCliques.
+
+ Args:
+ substituter: a grit.util.Substituter object.
+ '''
+ new_skel = []
+ for chunk in self.skeleton_:
+ if isinstance(chunk, clique.MessageClique):
+ old_message = chunk.GetMessage()
+ new_message = substituter.SubstituteMessage(old_message)
+ if new_message is not old_message:
+ new_skel.append(self.uberclique.MakeClique(new_message))
+ continue
+ new_skel.append(chunk)
+ self.skeleton_ = new_skel
diff --git a/tools/grit/grit/gather/tr_html_unittest.py b/tools/grit/grit/gather/tr_html_unittest.py
new file mode 100644
index 0000000000..1194853d9a
--- /dev/null
+++ b/tools/grit/grit/gather/tr_html_unittest.py
@@ -0,0 +1,524 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.tr_html'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+import six
+from six import StringIO
+
+from grit.gather import tr_html
+from grit import clique
+from grit import util
+
+
+class ParserUnittest(unittest.TestCase):
+ def testChunkingWithoutFoldWhitespace(self):
+ self.VerifyChunking(False)
+
+ def testChunkingWithFoldWhitespace(self):
+ self.VerifyChunking(True)
+
+ def VerifyChunking(self, fold_whitespace):
+ """Use a single function to run all chunking testing.
+
+ This makes it easier to run chunking with fold_whitespace both on and off,
+ to make sure the outputs are the same.
+
+ Args:
+ fold_whitespace: Whether whitespace sequences should be folded into a
+ single space.
+ """
+ self.VerifyChunkingBasic(fold_whitespace)
+ self.VerifyChunkingDescriptions(fold_whitespace)
+ self.VerifyChunkingReplaceables(fold_whitespace)
+ self.VerifyChunkingLineBreaks(fold_whitespace)
+ self.VerifyChunkingMessageBreak(fold_whitespace)
+ self.VerifyChunkingMessageNoBreak(fold_whitespace)
+
+ def VerifyChunkingBasic(self, fold_whitespace):
+ p = tr_html.HtmlChunks()
+ chunks = p.Parse('<p>Hello <b>dear</b> how <i>are</i>you?<p>Fine!',
+ fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (False, '<p>', ''), (True, 'Hello <b>dear</b> how <i>are</i>you?', ''),
+ (False, '<p>', ''), (True, 'Fine!', '')])
+
+ chunks = p.Parse('<p> Hello <b>dear</b> how <i>are</i>you? <p>Fine!',
+ fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (False, '<p> ', ''), (True, 'Hello <b>dear</b> how <i>are</i>you?', ''),
+ (False, ' <p>', ''), (True, 'Fine!', '')])
+
+ chunks = p.Parse('<p> Hello <b>dear how <i>are you? <p> Fine!',
+ fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (False, '<p> ', ''), (True, 'Hello <b>dear how <i>are you?', ''),
+ (False, ' <p> ', ''), (True, 'Fine!', '')])
+
+ # Ensure translateable sections that start with inline tags contain
+ # the starting inline tag.
+ chunks = p.Parse('<b>Hello!</b> how are you?<p><i>I am fine.</i>',
+ fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (True, '<b>Hello!</b> how are you?', ''), (False, '<p>', ''),
+ (True, '<i>I am fine.</i>', '')])
+
+ # Ensure translateable sections that end with inline tags contain
+ # the ending inline tag.
+ chunks = p.Parse("Hello! How are <b>you?</b><p><i>I'm fine!</i>",
+ fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (True, 'Hello! How are <b>you?</b>', ''), (False, '<p>', ''),
+ (True, "<i>I'm fine!</i>", '')])
+
+ def VerifyChunkingDescriptions(self, fold_whitespace):
+ p = tr_html.HtmlChunks()
+ # Check capitals and explicit descriptions
+ chunks = p.Parse('<!-- desc=bingo! --><B>Hello!</B> how are you?<P>'
+ '<I>I am fine.</I>', fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (True, '<B>Hello!</B> how are you?', 'bingo!'), (False, '<P>', ''),
+ (True, '<I>I am fine.</I>', '')])
+ chunks = p.Parse('<B><!-- desc=bingo! -->Hello!</B> how are you?<P>'
+ '<I>I am fine.</I>', fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (True, '<B>Hello!</B> how are you?', 'bingo!'), (False, '<P>', ''),
+ (True, '<I>I am fine.</I>', '')])
+ # Linebreaks get handled by the tclib message.
+ chunks = p.Parse('<B>Hello!</B> <!-- desc=bi\nngo\n! -->how are you?<P>'
+ '<I>I am fine.</I>', fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (True, '<B>Hello!</B> how are you?', 'bi\nngo\n!'), (False, '<P>', ''),
+ (True, '<I>I am fine.</I>', '')])
+
+ # In this case, because the explicit description appears after the first
+ # translateable, it will actually apply to the second translateable.
+ chunks = p.Parse('<B>Hello!</B> how are you?<!-- desc=bingo! --><P>'
+ '<I>I am fine.</I>', fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (True, '<B>Hello!</B> how are you?', ''), (False, '<P>', ''),
+ (True, '<I>I am fine.</I>', 'bingo!')])
+
+ def VerifyChunkingReplaceables(self, fold_whitespace):
+ # Check that replaceables within block tags (where attributes would go) are
+ # handled correctly.
+ p = tr_html.HtmlChunks()
+ chunks = p.Parse('<b>Hello!</b> how are you?<p [BINGO] [$~BONGO~$]>'
+ '<i>I am fine.</i>', fold_whitespace)
+ self.failUnlessEqual(chunks, [
+ (True, '<b>Hello!</b> how are you?', ''),
+ (False, '<p [BINGO] [$~BONGO~$]>', ''),
+ (True, '<i>I am fine.</i>', '')])
+
+ def VerifyChunkingLineBreaks(self, fold_whitespace):
+ # Check that the contents of preformatted tags preserve line breaks.
+ p = tr_html.HtmlChunks()
+ chunks = p.Parse('<textarea>Hello\nthere\nhow\nare\nyou?</textarea>',
+ fold_whitespace)
+ self.failUnlessEqual(chunks, [(False, '<textarea>', ''),
+ (True, 'Hello\nthere\nhow\nare\nyou?', ''), (False, '</textarea>', '')])
+
+ # ...and that other tags' line breaks are converted to spaces
+ chunks = p.Parse('<p>Hello\nthere\nhow\nare\nyou?</p>', fold_whitespace)
+ self.failUnlessEqual(chunks, [(False, '<p>', ''),
+ (True, 'Hello there how are you?', ''), (False, '</p>', '')])
+
+ def VerifyChunkingMessageBreak(self, fold_whitespace):
+ p = tr_html.HtmlChunks()
+ # Make sure that message-break comments work properly.
+ chunks = p.Parse('Break<!-- message-break --> apart '
+ '<!--message-break-->messages', fold_whitespace)
+ self.failUnlessEqual(chunks, [(True, 'Break', ''),
+ (False, ' ', ''),
+ (True, 'apart', ''),
+ (False, ' ', ''),
+ (True, 'messages', '')])
+
+ # Make sure message-break comments work in an inline tag.
+ chunks = p.Parse('<a href=\'google.com\'><!-- message-break -->Google'
+ '<!--message-break--></a>', fold_whitespace)
+ self.failUnlessEqual(chunks, [(False, '<a href=\'google.com\'>', ''),
+ (True, 'Google', ''),
+ (False, '</a>', '')])
+
+ def VerifyChunkingMessageNoBreak(self, fold_whitespace):
+ p = tr_html.HtmlChunks()
+ # Make sure that message-no-break comments work properly.
+ chunks = p.Parse('Please <!-- message-no-break --> <br />don\'t break',
+ fold_whitespace)
+ self.failUnlessEqual(chunks, [(True, 'Please <!-- message-no-break --> '
+ '<br />don\'t break', '')])
+
+ chunks = p.Parse('Please <br /> break. <!-- message-no-break --> <br /> '
+ 'But not this time.', fold_whitespace)
+ self.failUnlessEqual(chunks, [(True, 'Please', ''),
+ (False, ' <br /> ', ''),
+ (True, 'break. <!-- message-no-break --> '
+ '<br /> But not this time.', '')])
+
+ def testTranslateableAttributes(self):
+ p = tr_html.HtmlChunks()
+
+ # Check that the translateable attributes in <img>, <submit>, <button> and
+ # <text> elements buttons are handled correctly.
+ chunks = p.Parse('<img src=bingo.jpg alt="hello there">'
+ '<input type=submit value="hello">'
+ '<input type="button" value="hello">'
+ '<input type=\'text\' value=\'Howdie\'>', False)
+ self.failUnlessEqual(chunks, [
+ (False, '<img src=bingo.jpg alt="', ''), (True, 'hello there', ''),
+ (False, '"><input type=submit value="', ''), (True, 'hello', ''),
+ (False, '"><input type="button" value="', ''), (True, 'hello', ''),
+ (False, '"><input type=\'text\' value=\'', ''), (True, 'Howdie', ''),
+ (False, '\'>', '')])
+
+
+ def testTranslateableHtmlToMessage(self):
+ msg = tr_html.HtmlToMessage(
+ 'Hello <b>[USERNAME]</b>, <how> <i>are</i> you?')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres ==
+ 'Hello BEGIN_BOLDX_USERNAME_XEND_BOLD, '
+ '<how> BEGIN_ITALICareEND_ITALIC you?')
+
+ msg = tr_html.HtmlToMessage('<b>Hello</b><I>Hello</I><b>Hello</b>')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres ==
+ 'BEGIN_BOLD_1HelloEND_BOLD_1BEGIN_ITALICHelloEND_ITALIC'
+ 'BEGIN_BOLD_2HelloEND_BOLD_2')
+
+ # Check that nesting (of the <font> tags) is handled correctly - i.e. that
+ # the closing placeholder numbers match the opening placeholders.
+ msg = tr_html.HtmlToMessage(
+ '''<font size=-1><font color=#FF0000>Update!</font> '''
+ '''<a href='http://desktop.google.com/whatsnew.html?hl=[$~LANG~$]'>'''
+ '''New Features</a>: Now search PDFs, MP3s, Firefox web history, and '''
+ '''more</font>''')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres ==
+ 'BEGIN_FONT_1BEGIN_FONT_2Update!END_FONT_2 BEGIN_LINK'
+ 'New FeaturesEND_LINK: Now search PDFs, MP3s, Firefox '
+ 'web history, and moreEND_FONT_1')
+
+ msg = tr_html.HtmlToMessage('''<a href='[$~URL~$]'><b>[NUM][CAT]</b></a>''')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres == 'BEGIN_LINKBEGIN_BOLDX_NUM_XX_CAT_XEND_BOLDEND_LINK')
+
+ msg = tr_html.HtmlToMessage(
+ '''<font size=-1><a class=q onClick='return window.qs?qs(this):1' '''
+ '''href='http://[WEBSERVER][SEARCH_URI]'>Desktop</a></font> '''
+ ''' ''')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres ==
+ '''BEGIN_FONTBEGIN_LINKDesktopEND_LINKEND_FONTSPACE''')
+
+ msg = tr_html.HtmlToMessage(
+ '''<br><br><center><font size=-2>©2005 Google </font></center>''', 1)
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres ==
+ u'BEGIN_BREAK_1BEGIN_BREAK_2BEGIN_CENTERBEGIN_FONT\xa92005'
+ u' Google END_FONTEND_CENTER')
+
+ msg = tr_html.HtmlToMessage(
+ ''' - <a class=c href=[$~CACHE~$]>Cached</a>''')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres ==
+ ' - BEGIN_LINKCachedEND_LINK')
+
+ # Check that upper-case tags are handled correctly.
+ msg = tr_html.HtmlToMessage(
+ '''You can read the <A HREF='http://desktop.google.com/privacypolicy.'''
+ '''html?hl=[LANG_CODE]'>Privacy Policy</A> and <A HREF='http://desktop'''
+ '''.google.com/privacyfaq.html?hl=[LANG_CODE]'>Privacy FAQ</A> online.''')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres ==
+ 'You can read the BEGIN_LINK_1Privacy PolicyEND_LINK_1 and '
+ 'BEGIN_LINK_2Privacy FAQEND_LINK_2 online.')
+
+ # Check that tags with linebreaks immediately preceding them are handled
+ # correctly.
+ msg = tr_html.HtmlToMessage(
+ '''You can read the
+<A HREF='http://desktop.google.com/privacypolicy.html?hl=[LANG_CODE]'>Privacy Policy</A>
+and <A HREF='http://desktop.google.com/privacyfaq.html?hl=[LANG_CODE]'>Privacy FAQ</A> online.''')
+ pres = msg.GetPresentableContent()
+ self.failUnless(pres == '''You can read the
+BEGIN_LINK_1Privacy PolicyEND_LINK_1
+and BEGIN_LINK_2Privacy FAQEND_LINK_2 online.''')
+
+ # Check that message-no-break comments are handled correctly.
+ msg = tr_html.HtmlToMessage('''Please <!-- message-no-break --><br /> don't break''')
+ pres = msg.GetPresentableContent()
+ self.failUnlessEqual(pres, '''Please BREAK don't break''')
+
+class TrHtmlUnittest(unittest.TestCase):
+ def testSetAttributes(self):
+ html = tr_html.TrHtml(StringIO(''))
+ self.failUnlessEqual(html.fold_whitespace_, False)
+ html.SetAttributes({})
+ self.failUnlessEqual(html.fold_whitespace_, False)
+ html.SetAttributes({'fold_whitespace': 'false'})
+ self.failUnlessEqual(html.fold_whitespace_, False)
+ html.SetAttributes({'fold_whitespace': 'true'})
+ self.failUnlessEqual(html.fold_whitespace_, True)
+
+ def testFoldWhitespace(self):
+ text = '<td> Test Message </td>'
+
+ html = tr_html.TrHtml(StringIO(text))
+ html.Parse()
+ self.failUnlessEqual(html.skeleton_[1].GetMessage().GetPresentableContent(),
+ 'Test Message')
+
+ html = tr_html.TrHtml(StringIO(text))
+ html.fold_whitespace_ = True
+ html.Parse()
+ self.failUnlessEqual(html.skeleton_[1].GetMessage().GetPresentableContent(),
+ 'Test Message')
+
+ def testTable(self):
+ html = tr_html.TrHtml(StringIO('''<table class="shaded-header"><tr>
+<td class="header-element b expand">Preferences</td>
+<td class="header-element s">
+<a href="http://desktop.google.com/preferences.html">Preferences Help</a>
+</td>
+</tr></table>'''))
+ html.Parse()
+ self.failUnless(html.skeleton_[3].GetMessage().GetPresentableContent() ==
+ 'BEGIN_LINKPreferences HelpEND_LINK')
+
+ def testSubmitAttribute(self):
+ html = tr_html.TrHtml(StringIO('''</td>
+<td class="header-element"><input type=submit value="Save Preferences"
+name=submit2></td>
+</tr></table>'''))
+ html.Parse()
+ self.failUnless(html.skeleton_[1].GetMessage().GetPresentableContent() ==
+ 'Save Preferences')
+
+ def testWhitespaceAfterInlineTag(self):
+ '''Test that even if there is whitespace after an inline tag at the start
+ of a translateable section the inline tag will be included.
+ '''
+ html = tr_html.TrHtml(
+ StringIO('''<label for=DISPLAYNONE><font size=-1> Hello</font>'''))
+ html.Parse()
+ self.failUnless(html.skeleton_[1].GetMessage().GetRealContent() ==
+ '<font size=-1> Hello</font>')
+
+ def testSillyHeader(self):
+ html = tr_html.TrHtml(StringIO('''[!]
+title\tHello
+bingo
+bongo
+bla
+
+<p>Other stuff</p>'''))
+ html.Parse()
+ content = html.skeleton_[1].GetMessage().GetRealContent()
+ self.failUnless(content == 'Hello')
+ self.failUnless(html.skeleton_[-1] == '</p>')
+ # Right after the translateable the nontranslateable should start with
+ # a linebreak (this catches a bug we had).
+ self.failUnless(html.skeleton_[2][0] == '\n')
+
+
+ def testExplicitDescriptions(self):
+ html = tr_html.TrHtml(
+ StringIO('Hello [USER]<br/><!-- desc=explicit -->'
+ '<input type="button">Go!</input>'))
+ html.Parse()
+ msg = html.GetCliques()[1].GetMessage()
+ self.failUnlessEqual(msg.GetDescription(), 'explicit')
+ self.failUnlessEqual(msg.GetRealContent(), 'Go!')
+
+ html = tr_html.TrHtml(
+ StringIO('Hello [USER]<br/><!-- desc=explicit\nmultiline -->'
+ '<input type="button">Go!</input>'))
+ html.Parse()
+ msg = html.GetCliques()[1].GetMessage()
+ self.failUnlessEqual(msg.GetDescription(), 'explicit multiline')
+ self.failUnlessEqual(msg.GetRealContent(), 'Go!')
+
+
+ def testRegressionInToolbarAbout(self):
+ html = tr_html.TrHtml(util.PathFromRoot(r'grit/testdata/toolbar_about.html'))
+ html.Parse()
+ cliques = html.GetCliques()
+ for cl in cliques:
+ content = cl.GetMessage().GetRealContent()
+ if content.count('De parvis grandis acervus erit'):
+ self.failIf(content.count('$/translate'))
+
+
+ def HtmlFromFileWithManualCheck(self, f):
+ html = tr_html.TrHtml(f)
+ html.Parse()
+
+ # For manual results inspection only...
+ list = []
+ for item in html.skeleton_:
+ if isinstance(item, six.string_types):
+ list.append(item)
+ else:
+ list.append(item.GetMessage().GetPresentableContent())
+
+ return html
+
+
+ def testPrivacyHtml(self):
+ html = self.HtmlFromFileWithManualCheck(
+ util.PathFromRoot(r'grit/testdata/privacy.html'))
+
+ self.failUnless(html.skeleton_[1].GetMessage().GetRealContent() ==
+ 'Privacy and Google Desktop Search')
+ self.failUnless(html.skeleton_[3].startswith('<'))
+ self.failUnless(len(html.skeleton_) > 10)
+
+
+ def testPreferencesHtml(self):
+ html = self.HtmlFromFileWithManualCheck(
+ util.PathFromRoot(r'grit/testdata/preferences.html'))
+
+ # Verify that we don't get '[STATUS-MESSAGE]' as the original content of
+ # one of the MessageClique objects (it would be a placeholder-only message
+ # and we're supposed to have stripped those).
+
+ for item in [x for x in html.skeleton_
+ if isinstance(x, clique.MessageClique)]:
+ if (item.GetMessage().GetRealContent() == '[STATUS-MESSAGE]' or
+ item.GetMessage().GetRealContent() == '[ADDIN-DO] [ADDIN-OPTIONS]'):
+ self.fail()
+
+ self.failUnless(len(html.skeleton_) > 100)
+
+ def AssertNumberOfTranslateables(self, files, num):
+ '''Fails if any of the files in files don't have exactly
+ num translateable sections.
+
+ Args:
+ files: ['file1', 'file2']
+ num: 3
+ '''
+ for f in files:
+ f = util.PathFromRoot(r'grit/testdata/%s' % f)
+ html = self.HtmlFromFileWithManualCheck(f)
+ self.failUnless(len(html.GetCliques()) == num)
+
+ def testFewTranslateables(self):
+ self.AssertNumberOfTranslateables(['browser.html', 'email_thread.html',
+ 'header.html', 'mini.html',
+ 'oneclick.html', 'script.html',
+ 'time_related.html', 'versions.html'], 0)
+ self.AssertNumberOfTranslateables(['footer.html', 'hover.html'], 1)
+
+ def testOtherHtmlFilesForManualInspection(self):
+ files = [
+ 'about.html', 'bad_browser.html', 'cache_prefix.html',
+ 'cache_prefix_file.html', 'chat_result.html', 'del_footer.html',
+ 'del_header.html', 'deleted.html', 'details.html', 'email_result.html',
+ 'error.html', 'explicit_web.html', 'footer.html',
+ 'homepage.html', 'indexing_speed.html',
+ 'install_prefs.html', 'install_prefs2.html',
+ 'oem_enable.html', 'oem_non_admin.html', 'onebox.html',
+ 'password.html', 'quit_apps.html', 'recrawl.html',
+ 'searchbox.html', 'sidebar_h.html', 'sidebar_v.html', 'status.html',
+ ]
+ for f in files:
+ self.HtmlFromFileWithManualCheck(
+ util.PathFromRoot(r'grit/testdata/%s' % f))
+
+ def testTranslate(self):
+ # Note that the English translation of documents that use character
+ # literals (e.g. ©) will not be the same as the original document
+ # because the character literal will be transformed into the Unicode
+ # character itself. So for this test we choose some relatively complex
+ # HTML without character entities (but with because that's handled
+ # specially).
+ html = tr_html.TrHtml(StringIO(''' <script>
+ <!--
+ function checkOffice() { var w = document.getElementById("h7");
+ var e = document.getElementById("h8"); var o = document.getElementById("h10");
+ if (!(w.checked || e.checked)) { o.checked=0;o.disabled=1;} else {o.disabled=0;} }
+ // -->
+ </script>
+ <input type=checkbox [CHECK-DOC] name=DOC id=h7 onclick='checkOffice()'>
+ <label for=h7> Word</label><br>
+ <input type=checkbox [CHECK-XLS] name=XLS id=h8 onclick='checkOffice()'>
+ <label for=h8> Excel</label><br>
+ <input type=checkbox [CHECK-PPT] name=PPT id=h9>
+ <label for=h9> PowerPoint</label><br>
+ </span></td><td nowrap valign=top><span class="s">
+ <input type=checkbox [CHECK-PDF] name=PDF id=hpdf>
+ <label for=hpdf> PDF</label><br>
+ <input type=checkbox [CHECK-TXT] name=TXT id=h6>
+ <label for=h6> Text, media, and other files</label><br>
+ </tr>
+ <tr><td nowrap valign=top colspan=3><span class="s"><br />
+ <input type=checkbox [CHECK-SECUREOFFICE] name=SECUREOFFICE id=h10>
+ <label for=h10> Password-protected Office documents (Word, Excel)</label><br />
+ <input type=checkbox [DISABLED-HTTPS] [CHECK-HTTPS] name=HTTPS id=h12><label
+ for=h12> Secure pages (HTTPS) in web history</label></span></td></tr>
+ </table>'''))
+ html.Parse()
+ trans = html.Translate('en')
+ if (html.GetText() != trans):
+ self.fail()
+
+
+ def testHtmlToMessageWithBlockTags(self):
+ msg = tr_html.HtmlToMessage(
+ 'Hello<p>Howdie<img alt="bingo" src="image.gif">', True)
+ result = msg.GetPresentableContent()
+ self.failUnless(
+ result == 'HelloBEGIN_PARAGRAPHHowdieBEGIN_BLOCKbingoEND_BLOCK')
+
+ msg = tr_html.HtmlToMessage(
+ 'Hello<p>Howdie<input type="button" value="bingo">', True)
+ result = msg.GetPresentableContent()
+ self.failUnless(
+ result == 'HelloBEGIN_PARAGRAPHHowdieBEGIN_BLOCKbingoEND_BLOCK')
+
+
+ def testHtmlToMessageRegressions(self):
+ msg = tr_html.HtmlToMessage(' - ', True)
+ result = msg.GetPresentableContent()
+ self.failUnless(result == ' - ')
+
+
+ def testEscapeUnescaped(self):
+ text = '© & "<hello>"'
+ unescaped = util.UnescapeHtml(text)
+ self.failUnless(unescaped == u'\u00a9\u00a0 & "<hello>"')
+ escaped_unescaped = util.EscapeHtml(unescaped, True)
+ self.failUnless(escaped_unescaped ==
+ u'\u00a9\u00a0 & "<hello>"')
+
+ def testRegressionCjkHtmlFile(self):
+ # TODO(joi) Fix this problem where unquoted attributes that
+ # have a value that is CJK characters causes the regular expression
+ # match never to return. (culprit is the _ELEMENT regexp(
+ if False:
+ html = self.HtmlFromFileWithManualCheck(util.PathFromRoot(
+ r'grit/testdata/ko_oem_enable_bug.html'))
+ self.failUnless(True)
+
+ def testRegressionCpuHang(self):
+ # If this regression occurs, the unit test will never return
+ html = tr_html.TrHtml(StringIO(
+ '''<input type=text size=12 id=advFileTypeEntry [~SHOW-FILETYPE-BOX~] value="[EXT]" name=ext>'''))
+ html.Parse()
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/gather/txt.py b/tools/grit/grit/gather/txt.py
new file mode 100644
index 0000000000..e5c10abc28
--- /dev/null
+++ b/tools/grit/grit/gather/txt.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Supports making amessage from a text file.
+'''
+
+from __future__ import print_function
+
+from grit.gather import interface
+from grit import tclib
+
+
+class TxtFile(interface.GathererBase):
+ '''A text file gatherer. Very simple, all text from the file becomes a
+ single clique.
+ '''
+
+ def Parse(self):
+ self.text_ = self._LoadInputFile()
+ self.clique_ = self.uberclique.MakeClique(tclib.Message(text=self.text_))
+
+ def GetText(self):
+ '''Returns the text of what is being gathered.'''
+ return self.text_
+
+ def GetTextualIds(self):
+ return [self.extkey]
+
+ def GetCliques(self):
+ '''Returns the MessageClique objects for all translateable portions.'''
+ return [self.clique_]
+
+ def Translate(self, lang, pseudo_if_not_available=True,
+ skeleton_gatherer=None, fallback_to_english=False):
+ return self.clique_.MessageForLanguage(lang,
+ pseudo_if_not_available,
+ fallback_to_english).GetRealContent()
diff --git a/tools/grit/grit/gather/txt_unittest.py b/tools/grit/grit/gather/txt_unittest.py
new file mode 100644
index 0000000000..abb9ed98d7
--- /dev/null
+++ b/tools/grit/grit/gather/txt_unittest.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for TxtFile gatherer'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import txt
+
+
+class TxtUnittest(unittest.TestCase):
+ def testGather(self):
+ input = StringIO('Hello there\nHow are you?')
+ gatherer = txt.TxtFile(input)
+ gatherer.Parse()
+ self.failUnless(gatherer.GetText() == input.getvalue())
+ self.failUnless(len(gatherer.GetCliques()) == 1)
+ self.failUnless(gatherer.GetCliques()[0].GetMessage().GetRealContent() ==
+ input.getvalue())
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/grd_reader.py b/tools/grit/grit/grd_reader.py
new file mode 100644
index 0000000000..b7bb782977
--- /dev/null
+++ b/tools/grit/grit/grd_reader.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Class for reading GRD files into memory, without processing them.
+'''
+
+from __future__ import print_function
+
+import os.path
+import sys
+import xml.sax
+import xml.sax.handler
+
+import six
+
+from grit import exception
+from grit import util
+from grit.node import mapping
+from grit.node import misc
+
+
+class StopParsingException(Exception):
+ '''An exception used to stop parsing.'''
+ pass
+
+
+class GrdContentHandler(xml.sax.handler.ContentHandler):
+ def __init__(self, stop_after, debug, dir, defines, tags_to_ignore,
+ target_platform, source):
+ # Invariant of data:
+ # 'root' is the root of the parse tree being created, or None if we haven't
+ # parsed out any elements.
+ # 'stack' is the a stack of elements that we push new nodes onto and
+ # pop from when they finish parsing, or [] if we are not currently parsing.
+ # 'stack[-1]' is the top of the stack.
+ self.root = None
+ self.stack = []
+ self.stop_after = stop_after
+ self.debug = debug
+ self.dir = dir
+ self.defines = defines
+ self.tags_to_ignore = tags_to_ignore or set()
+ self.ignore_depth = 0
+ self.target_platform = target_platform
+ self.source = source
+
+ def startElement(self, name, attrs):
+ if self.ignore_depth or name in self.tags_to_ignore:
+ if self.debug and self.ignore_depth == 0:
+ print("Ignoring element %s and its children" % name)
+ self.ignore_depth += 1
+ return
+
+ if self.debug:
+ attr_list = ' '.join('%s="%s"' % kv for kv in attrs.items())
+ print("Starting parsing of element %s with attributes %r" %
+ (name, attr_list or '(none)'))
+
+ typeattr = attrs.get('type')
+ node = mapping.ElementToClass(name, typeattr)()
+ node.source = self.source
+
+ if self.stack:
+ self.stack[-1].AddChild(node)
+ node.StartParsing(name, self.stack[-1])
+ else:
+ assert self.root is None
+ self.root = node
+ if isinstance(self.root, misc.GritNode):
+ if self.target_platform:
+ self.root.SetTargetPlatform(self.target_platform)
+ node.StartParsing(name, None)
+ if self.defines:
+ node.SetDefines(self.defines)
+ self.stack.append(node)
+
+ for attr, attrval in attrs.items():
+ node.HandleAttribute(attr, attrval)
+
+ def endElement(self, name):
+ if self.ignore_depth:
+ self.ignore_depth -= 1
+ return
+
+ if name == 'part':
+ partnode = self.stack[-1]
+ partnode.started_inclusion = True
+ # Add the contents of the sub-grd file as children of the <part> node.
+ partname = os.path.join(self.dir, partnode.GetInputPath())
+ # Check the GRDP file exists.
+ if not os.path.exists(partname):
+ raise exception.FileNotFound(partname)
+ # Exceptions propagate to the handler in grd_reader.Parse().
+ oldsource = self.source
+ try:
+ self.source = partname
+ xml.sax.parse(partname, GrdPartContentHandler(self))
+ finally:
+ self.source = oldsource
+
+ if self.debug:
+ print("End parsing of element %s" % name)
+ self.stack.pop().EndParsing()
+
+ if name == self.stop_after:
+ raise StopParsingException()
+
+ def characters(self, content):
+ if self.ignore_depth == 0:
+ if self.stack[-1]:
+ self.stack[-1].AppendContent(content)
+
+ def ignorableWhitespace(self, whitespace):
+ # TODO(joi): This is not supported by expat. Should use a different XML
+ # parser?
+ pass
+
+
+class GrdPartContentHandler(xml.sax.handler.ContentHandler):
+ def __init__(self, parent):
+ self.parent = parent
+ self.depth = 0
+
+ def startElement(self, name, attrs):
+ if self.depth:
+ self.parent.startElement(name, attrs)
+ else:
+ if name != 'grit-part':
+ raise exception.MissingElement("root tag must be <grit-part>")
+ if attrs:
+ raise exception.UnexpectedAttribute(
+ "<grit-part> tag must not have attributes")
+ self.depth += 1
+
+ def endElement(self, name):
+ self.depth -= 1
+ if self.depth:
+ self.parent.endElement(name)
+
+ def characters(self, content):
+ self.parent.characters(content)
+
+ def ignorableWhitespace(self, whitespace):
+ self.parent.ignorableWhitespace(whitespace)
+
+
+def Parse(filename_or_stream, dir=None, stop_after=None, first_ids_file=None,
+ debug=False, defines=None, tags_to_ignore=None, target_platform=None,
+ predetermined_ids_file=None):
+ '''Parses a GRD file into a tree of nodes (from grit.node).
+
+ If filename_or_stream is a stream, 'dir' should point to the directory
+ notionally containing the stream (this feature is only used in unit tests).
+
+ If 'stop_after' is provided, the parsing will stop once the first node
+ with this name has been fully parsed (including all its contents).
+
+ If 'debug' is true, lots of information about the parsing events will be
+ printed out during parsing of the file.
+
+ If 'first_ids_file' is non-empty, it is used to override the setting for the
+ first_ids_file attribute of the <grit> root node. Note that the first_ids_file
+ parameter should be relative to the cwd, even though the first_ids_file
+ attribute of the <grit> node is relative to the grd file.
+
+ If 'target_platform' is set, this is used to determine the target
+ platform of builds, instead of using |sys.platform|.
+
+ Args:
+ filename_or_stream: './bla.xml'
+ dir: None (if filename_or_stream is a filename) or '.'
+ stop_after: 'inputs'
+ first_ids_file: 'GRIT_DIR/../gritsettings/resource_ids'
+ debug: False
+ defines: dictionary of defines, like {'chromeos': '1'}
+ target_platform: None or the value that would be returned by sys.platform
+ on your target platform.
+ predetermined_ids_file: File path to a file containing a pre-determined
+ mapping from resource names to resource ids which will be used to assign
+ resource ids to those resources.
+
+ Return:
+ Subclass of grit.node.base.Node
+
+ Throws:
+ grit.exception.Parsing
+ '''
+
+ if isinstance(filename_or_stream, six.string_types):
+ source = filename_or_stream
+ if dir is None:
+ dir = util.dirname(filename_or_stream)
+ else:
+ source = None
+
+ handler = GrdContentHandler(stop_after=stop_after, debug=debug, dir=dir,
+ defines=defines, tags_to_ignore=tags_to_ignore,
+ target_platform=target_platform, source=source)
+ try:
+ xml.sax.parse(filename_or_stream, handler)
+ except StopParsingException:
+ assert stop_after
+ pass
+ except:
+ if not debug:
+ print("parse exception: run GRIT with the -x flag to debug .grd problems")
+ raise
+
+ if handler.root.name != 'grit':
+ raise exception.MissingElement("root tag must be <grit>")
+
+ if hasattr(handler.root, 'SetOwnDir'):
+ # Fix up the base_dir so it is relative to the input file.
+ assert dir is not None
+ handler.root.SetOwnDir(dir)
+
+ if isinstance(handler.root, misc.GritNode):
+ handler.root.SetPredeterminedIdsFile(predetermined_ids_file)
+ if first_ids_file:
+ # Make the path to the first_ids_file relative to the grd file,
+ # unless it begins with GRIT_DIR.
+ GRIT_DIR_PREFIX = 'GRIT_DIR'
+ if not (first_ids_file.startswith(GRIT_DIR_PREFIX)
+ and first_ids_file[len(GRIT_DIR_PREFIX)] in ['/', '\\']):
+ rel_dir = os.path.relpath(os.getcwd(), dir)
+ first_ids_file = util.normpath(os.path.join(rel_dir, first_ids_file))
+ handler.root.attrs['first_ids_file'] = first_ids_file
+ # Assign first ids to the nodes that don't have them.
+ handler.root.AssignFirstIds(filename_or_stream, defines)
+
+ return handler.root
+
+
+if __name__ == '__main__':
+ util.ChangeStdoutEncoding()
+ print(six.text_type(Parse(sys.argv[1])))
diff --git a/tools/grit/grit/grd_reader_unittest.py b/tools/grit/grit/grd_reader_unittest.py
new file mode 100644
index 0000000000..920a92f9c0
--- /dev/null
+++ b/tools/grit/grit/grd_reader_unittest.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grd_reader package'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import unittest
+
+import six
+from six import StringIO
+
+from grit import exception
+from grit import grd_reader
+from grit import util
+from grit.node import empty
+from grit.node import message
+
+
+class GrdReaderUnittest(unittest.TestCase):
+ def testParsingAndXmlOutput(self):
+ input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit base_dir="." current_release="3" latest_public_release="2" source_lang_id="en-US">
+ <release seq="3">
+ <includes>
+ <include file="images/logo.gif" name="ID_LOGO" type="gif" />
+ </includes>
+ <messages>
+ <if expr="True">
+ <message desc="Printed to greet the currently logged in user" name="IDS_GREETING">
+ Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+ </message>
+ </if>
+ </messages>
+ <structures>
+ <structure file="rc_files/dialogs.rc" name="IDD_NARROW_DIALOG" type="dialog">
+ <skeleton expr="lang == 'fr-FR'" file="bla.rc" variant_of_revision="3" />
+ </structure>
+ <structure file="rc_files/version.rc" name="VS_VERSION_INFO" type="version" />
+ </structures>
+ </release>
+ <translations>
+ <file lang="nl" path="nl_translations.xtb" />
+ </translations>
+ <outputs>
+ <output filename="resource.h" type="rc_header" />
+ <output filename="resource.rc" lang="en-US" type="rc_all" />
+ </outputs>
+</grit>'''
+ pseudo_file = StringIO(input)
+ tree = grd_reader.Parse(pseudo_file, '.')
+ output = six.text_type(tree)
+ expected_output = input.replace(u' base_dir="."', u'')
+ self.assertEqual(expected_output, output)
+ self.failUnless(tree.GetNodeById('IDS_GREETING'))
+
+
+ def testStopAfter(self):
+ input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+ <outputs>
+ <output filename="resource.h" type="rc_header" />
+ <output filename="resource.rc" lang="en-US" type="rc_all" />
+ </outputs>
+ <release seq="3">
+ <includes>
+ <include type="gif" name="ID_LOGO" file="images/logo.gif"/>
+ </includes>
+ </release>
+</grit>'''
+ pseudo_file = StringIO(input)
+ tree = grd_reader.Parse(pseudo_file, '.', stop_after='outputs')
+ # only an <outputs> child
+ self.failUnless(len(tree.children) == 1)
+ self.failUnless(tree.children[0].name == 'outputs')
+
+ def testLongLinesWithComments(self):
+ input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+ <release seq="3">
+ <messages>
+ <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+ This is a very long line with no linebreaks yes yes it stretches on <!--
+ -->and on <!--
+ -->and on!
+ </message>
+ </messages>
+ </release>
+</grit>'''
+ pseudo_file = StringIO(input)
+ tree = grd_reader.Parse(pseudo_file, '.')
+
+ greeting = tree.GetNodeById('IDS_GREETING')
+ self.failUnless(greeting.GetCliques()[0].GetMessage().GetRealContent() ==
+ 'This is a very long line with no linebreaks yes yes it '
+ 'stretches on and on and on!')
+
+ def doTestAssignFirstIds(self, first_ids_path):
+ input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3"
+ base_dir="." first_ids_file="%s">
+ <release seq="3">
+ <messages>
+ <message name="IDS_TEST" desc="test">
+ test
+ </message>
+ </messages>
+ </release>
+</grit>''' % first_ids_path
+ pseudo_file = StringIO(input)
+ grit_root_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
+ '..')
+ fake_input_path = os.path.join(
+ grit_root_dir, "grit/testdata/chrome/app/generated_resources.grd")
+ root = grd_reader.Parse(pseudo_file, os.path.split(fake_input_path)[0])
+ root.AssignFirstIds(fake_input_path, {})
+ messages_node = root.children[0].children[0]
+ self.failUnless(isinstance(messages_node, empty.MessagesNode))
+ self.failUnless(messages_node.attrs["first_id"] !=
+ empty.MessagesNode().DefaultAttributes()["first_id"])
+
+ def testAssignFirstIds(self):
+ self.doTestAssignFirstIds("../../tools/grit/resource_ids")
+
+ def testAssignFirstIdsUseGritDir(self):
+ self.doTestAssignFirstIds("GRIT_DIR/grit/testdata/tools/grit/resource_ids")
+
+ def testAssignFirstIdsMultipleMessages(self):
+ """If there are multiple messages sections, the resource_ids file
+ needs to list multiple first_id values."""
+ input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3"
+ base_dir="." first_ids_file="resource_ids">
+ <release seq="3">
+ <messages>
+ <message name="IDS_TEST" desc="test">
+ test
+ </message>
+ </messages>
+ <messages>
+ <message name="IDS_TEST2" desc="test">
+ test2
+ </message>
+ </messages>
+ </release>
+</grit>'''
+ pseudo_file = StringIO(input)
+ grit_root_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
+ '..')
+ fake_input_path = os.path.join(grit_root_dir, "grit/testdata/test.grd")
+
+ root = grd_reader.Parse(pseudo_file, os.path.split(fake_input_path)[0])
+ root.AssignFirstIds(fake_input_path, {})
+ messages_node = root.children[0].children[0]
+ self.assertTrue(isinstance(messages_node, empty.MessagesNode))
+ self.assertEqual('100', messages_node.attrs["first_id"])
+ messages_node = root.children[0].children[1]
+ self.assertTrue(isinstance(messages_node, empty.MessagesNode))
+ self.assertEqual('10000', messages_node.attrs["first_id"])
+
+ def testUseNameForIdAndPpIfdef(self):
+ input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+ <release seq="3">
+ <messages>
+ <if expr="pp_ifdef('hello')">
+ <message name="IDS_HELLO" use_name_for_id="true">
+ Hello!
+ </message>
+ </if>
+ </messages>
+ </release>
+</grit>'''
+ pseudo_file = StringIO(input)
+ root = grd_reader.Parse(pseudo_file, '.', defines={'hello': '1'})
+
+ # Check if the ID is set to the name. In the past, there was a bug
+ # that caused the ID to be a generated number.
+ hello = root.GetNodeById('IDS_HELLO')
+ self.failUnless(hello.GetCliques()[0].GetId() == 'IDS_HELLO')
+
+ def testUseNameForIdWithIfElse(self):
+ input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+ <release seq="3">
+ <messages>
+ <if expr="pp_ifdef('hello')">
+ <then>
+ <message name="IDS_HELLO" use_name_for_id="true">
+ Hello!
+ </message>
+ </then>
+ <else>
+ <message name="IDS_HELLO" use_name_for_id="true">
+ Yellow!
+ </message>
+ </else>
+ </if>
+ </messages>
+ </release>
+</grit>'''
+ pseudo_file = StringIO(input)
+ root = grd_reader.Parse(pseudo_file, '.', defines={'hello': '1'})
+
+ # Check if the ID is set to the name. In the past, there was a bug
+ # that caused the ID to be a generated number.
+ hello = root.GetNodeById('IDS_HELLO')
+ self.failUnless(hello.GetCliques()[0].GetId() == 'IDS_HELLO')
+
+ def testPartInclusionAndCorrectSource(self):
+ arbitrary_path_grd = u'''\
+ <grit-part>
+ <message name="IDS_TEST5" desc="test5">test5</message>
+ </grit-part>'''
+ tmp_dir = util.TempDir({'arbitrary_path.grp': arbitrary_path_grd})
+ arbitrary_path_grd_file = tmp_dir.GetPath('arbitrary_path.grp')
+ top_grd = u'''\
+ <grit latest_public_release="2" current_release="3">
+ <release seq="3">
+ <messages>
+ <message name="IDS_TEST" desc="test">
+ test
+ </message>
+ <part file="sub.grp" />
+ <part file="%s" />
+ </messages>
+ </release>
+ </grit>''' % arbitrary_path_grd_file
+ sub_grd = u'''\
+ <grit-part>
+ <message name="IDS_TEST2" desc="test2">test2</message>
+ <part file="subsub.grp" />
+ <message name="IDS_TEST3" desc="test3">test3</message>
+ </grit-part>'''
+ subsub_grd = u'''\
+ <grit-part>
+ <message name="IDS_TEST4" desc="test4">test4</message>
+ </grit-part>'''
+ expected_output = u'''\
+ <grit current_release="3" latest_public_release="2">
+ <release seq="3">
+ <messages>
+ <message desc="test" name="IDS_TEST">
+ test
+ </message>
+ <part file="sub.grp">
+ <message desc="test2" name="IDS_TEST2">
+ test2
+ </message>
+ <part file="subsub.grp">
+ <message desc="test4" name="IDS_TEST4">
+ test4
+ </message>
+ </part>
+ <message desc="test3" name="IDS_TEST3">
+ test3
+ </message>
+ </part>
+ <part file="%s">
+ <message desc="test5" name="IDS_TEST5">
+ test5
+ </message>
+ </part>
+ </messages>
+ </release>
+ </grit>''' % arbitrary_path_grd_file
+
+ with util.TempDir({'sub.grp': sub_grd,
+ 'subsub.grp': subsub_grd}) as tmp_sub_dir:
+ output = grd_reader.Parse(StringIO(top_grd),
+ tmp_sub_dir.GetPath())
+ correct_sources = {
+ 'IDS_TEST': None,
+ 'IDS_TEST2': tmp_sub_dir.GetPath('sub.grp'),
+ 'IDS_TEST3': tmp_sub_dir.GetPath('sub.grp'),
+ 'IDS_TEST4': tmp_sub_dir.GetPath('subsub.grp'),
+ 'IDS_TEST5': arbitrary_path_grd_file,
+ }
+
+ for node in output.ActiveDescendants():
+ with node:
+ if isinstance(node, message.MessageNode):
+ self.assertEqual(correct_sources[node.attrs.get('name')], node.source)
+ self.assertEqual(expected_output.split(), output.FormatXml().split())
+ tmp_dir.CleanUp()
+
+ def testPartInclusionFailure(self):
+ template = u'''
+ <grit latest_public_release="2" current_release="3">
+ <outputs>
+ %s
+ </outputs>
+ </grit>'''
+
+ part_failures = [
+ (exception.UnexpectedContent, u'<part file="x">fnord</part>'),
+ (exception.UnexpectedChild,
+ u'<part file="x"><output filename="x" type="y" /></part>'),
+ (exception.FileNotFound, u'<part file="yet_created_x" />'),
+ ]
+ for raises, data in part_failures:
+ data = StringIO(template % data)
+ self.assertRaises(raises, grd_reader.Parse, data, '.')
+
+ gritpart_failures = [
+ (exception.UnexpectedAttribute, u'<grit-part file="xyz"></grit-part>'),
+ (exception.MissingElement, u'<output filename="x" type="y" />'),
+ ]
+ for raises, data in gritpart_failures:
+ top_grd = StringIO(template % u'<part file="bad.grp" />')
+ with util.TempDir({'bad.grp': data}) as temp_dir:
+ self.assertRaises(raises, grd_reader.Parse, top_grd, temp_dir.GetPath())
+
+ def testEarlyEnoughPlatformSpecification(self):
+ # This is a regression test for issue
+ # https://code.google.com/p/grit-i18n/issues/detail?id=23
+ grd_text = u'''<?xml version="1.0" encoding="UTF-8"?>
+ <grit latest_public_release="1" current_release="1">
+ <release seq="1">
+ <messages>
+ <if expr="not pp_ifdef('use_titlecase')">
+ <message name="IDS_XYZ">foo</message>
+ </if>
+ <!-- The assumption is that use_titlecase is never true for
+ this platform. When the platform isn't set to 'android'
+ early enough, we get a duplicate message name. -->
+ <if expr="os == '%s'">
+ <message name="IDS_XYZ">boo</message>
+ </if>
+ </messages>
+ </release>
+ </grit>''' % sys.platform
+ with util.TempDir({}) as temp_dir:
+ grd_reader.Parse(StringIO(grd_text), temp_dir.GetPath(),
+ target_platform='android')
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/grit-todo.xml b/tools/grit/grit/grit-todo.xml
new file mode 100644
index 0000000000..b8c20fdfad
--- /dev/null
+++ b/tools/grit/grit/grit-todo.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="windows-1252"?>
+<TODOLIST FILEFORMAT="6" PROJECTNAME="GRIT" NEXTUNIQUEID="56" FILEVERSION="69" LASTMODIFIED="2005-08-19">
+ <TASK STARTDATESTRING="2005-04-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38453.49975694" TITLE="check 'name' attribute is unique" TIMEESTUNITS="H" ID="2" PERCENTDONE="100" STARTDATE="38450.00000000" DONEDATESTRING="2005-04-11" POS="22" DONEDATE="38453.00000000"/>
+ <TASK STARTDATESTRING="2005-04-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38488.48189815" TITLE="import id-calculating code" TIMEESTUNITS="H" ID="3" PERCENTDONE="100" STARTDATE="38450.00000000" DONEDATESTRING="2005-05-16" POS="13" DONEDATE="38488.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38488.48209491" TITLE="Import tool for existing translations" TIMEESTUNITS="H" ID="6" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="12" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00805556" TITLE="Export XMBs" TIMEESTUNITS="H" ID="8" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-08" POS="20" DONEDATE="38511.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00924769" TITLE="Initial Integration" TIMEESTUNITS="H" ID="10" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-08" POS="10" DONEDATE="38511.00000000">
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38496.54048611" TITLE="parser for %s strings" TIMEESTUNITS="H" ID="4" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-24" POS="2" DONEDATE="38496.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38497.00261574" TITLE="import tool for existing RC files" TIMEESTUNITS="H" ID="5" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-25" POS="4" DONEDATE="38497.00000000">
+ <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38496.92990741" TITLE="handle button value= and img alt= in message HTML text" TIMEESTUNITS="H" ID="22" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-05-24" POS="1" DONEDATE="38496.00000000"/>
+ <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38497.00258102" TITLE=" bug" TIMEESTUNITS="H" ID="23" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-05-25" POS="2" DONEDATE="38497.00000000"/>
+ </TASK>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38490.61171296" TITLE="grit build" TIMEESTUNITS="H" ID="7" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-18" POS="6" DONEDATE="38490.00000000">
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38490.61168981" TITLE="use IDs gathered from gatherers for .h file" TIMEESTUNITS="H" ID="20" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-18" POS="1" DONEDATE="38490.00000000"/>
+ </TASK>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38504.55199074" TITLE="SCons Integration" TIMEESTUNITS="H" ID="9" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-01" POS="1" DONEDATE="38504.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38490.61181713" TITLE="handle includes" TIMEESTUNITS="H" ID="12" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-18" POS="5" DONEDATE="38490.00000000"/>
+ <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38507.98567130" TITLE="output translated HTML templates" TIMEESTUNITS="H" ID="25" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-04" POS="3" DONEDATE="38507.00000000"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38507.99394676" TITLE="bug: re-escape too much in RC dialogs etc." TIMEESTUNITS="H" ID="38" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-04" POS="7" DONEDATE="38507.00000000"/>
+ </TASK>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46444444" TITLE="handle structure variants" TIMEESTUNITS="H" ID="11" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="15" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46456019" TITLE="handle include variants" TIMEESTUNITS="H" ID="13" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="17" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46537037" TITLE="handle translateable text for includes (e.g. image text)" TIMEESTUNITS="H" ID="14" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="14" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46712963" TITLE="ddoc" TIMEESTUNITS="H" ID="15" STARTDATE="38488.00000000" POS="4">
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46718750" TITLE="review comments miket" TIMEESTUNITS="H" ID="16" STARTDATE="38488.00000000" POS="2"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46722222" TITLE="review comments pdoyle" TIMEESTUNITS="H" ID="17" STARTDATE="38488.00000000" POS="1"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46732639" TITLE="remove 'extkey' from structure" TIMEESTUNITS="H" ID="18" STARTDATE="38488.00000000" POS="3"/>
+ <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.53537037" TITLE="add 'encoding' to structure" TIMEESTUNITS="H" ID="19" STARTDATE="38488.00000000" POS="6"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38503.55304398" TITLE="document limitation: emitter doesn't emit the translated HTML templates" TIMEESTUNITS="H" ID="30" STARTDATE="38503.00000000" POS="4"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38504.58541667" TITLE="add 'internal_comment' to <message>" TIMEESTUNITS="H" ID="32" STARTDATE="38503.00000000" POS="5"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.73391204" TITLE="<outputs> can not have paths (because of SCons integration - goes to build dir)" TIMEESTUNITS="H" ID="36" STARTDATE="38503.00000000" POS="9"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38506.64265046" TITLE="<identifers> and <identifier> nodes" TIMEESTUNITS="H" ID="37" STARTDATE="38503.00000000" POS="10"/>
+ <TASK STARTDATESTRING="2005-06-23" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38526.62344907" TITLE="<structure> can have 'exclude_from_rc' attribute (default false)" TIMEESTUNITS="H" ID="47" STARTDATE="38526.00000000" POS="8"/>
+ <TASK STARTDATESTRING="2005-06-23" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38531.94135417" TITLE="add 'enc_check' to <grit>" TIMEESTUNITS="H" ID="48" STARTDATE="38526.00000000" POS="7"/>
+ </TASK>
+ <TASK STARTDATESTRING="2005-05-18" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38492.51549769" TITLE="handle nontranslateable messages (in MessageClique?)" TIMEESTUNITS="H" ID="21" PERCENTDONE="100" STARTDATE="38490.00000000" DONEDATESTRING="2005-06-16" POS="16" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.70454861" TITLE="ask cprince about SCons builder in new mk system" TIMEESTUNITS="H" ID="24" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-02" POS="25" DONEDATE="38505.00000000"/>
+ <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38504.57436343" TITLE="fix AOL resource in trunk ("???????")" TIMEESTUNITS="H" ID="26" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-01" POS="19" DONEDATE="38504.00000000"/>
+ <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38498.53893519" TITLE="rc_all vs. rc_translateable vs. rc_nontranslateable" TIMEESTUNITS="H" ID="27" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-16" POS="6" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38509.45532407" TITLE="make separate .grb "outputs" file (and change SCons integ) (??)" TIMEESTUNITS="H" ID="28" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-06" POS="8" DONEDATE="38509.00000000"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00939815" TITLE="fix unit tests so they run from any directory" TIMEESTUNITS="H" ID="33" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-08" POS="18" DONEDATE="38511.00000000"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38508.96640046" TITLE="Change R4 tool to CC correct team(s) on GRIT changes" TIMEESTUNITS="H" ID="39" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-05" POS="23" DONEDATE="38508.00000000"/>
+ <TASK STARTDATESTRING="2005-06-07" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00881944" TITLE="Document why wrapper.rc" TIMEESTUNITS="H" ID="40" PERCENTDONE="100" STARTDATE="38510.00000000" DONEDATESTRING="2005-06-08" POS="21" DONEDATE="38511.00000000"/>
+ <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00804398" TITLE="import XTBs" TIMEESTUNITS="H" ID="41" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-16" POS="11" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00875000" TITLE="Nightly build integration" TIMEESTUNITS="H" ID="42" STARTDATE="38511.00000000" POS="3"/>
+ <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00891204" TITLE="BUGS" TIMEESTUNITS="H" ID="43" STARTDATE="38511.00000000" POS="24">
+ <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38513.03375000" TITLE="Should report error if RC-section structure refers to does not exist" TIMEESTUNITS="H" ID="44" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-10" POS="1" DONEDATE="38513.00000000"/>
+ </TASK>
+ <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00981481" TITLE="NEW FEATURES" TIMEESTUNITS="H" ID="45" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-16" POS="7" DONEDATE="38519.00000000">
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.70077546" TITLE="Implement line-continuation feature (\ at end of line?)" TIMEESTUNITS="H" ID="34" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-16" POS="1" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.70262731" TITLE="Implement conditional inclusion & reflect the conditionals from R3 RC file" TIMEESTUNITS="H" ID="35" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-16" POS="2" DONEDATE="38519.00000000"/>
+ </TASK>
+ <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.01046296" TITLE="TC integration (one-way TO the TC)" TIMEESTUNITS="H" ID="46" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-16" POS="5" DONEDATE="38519.00000000"/>
+ <TASK STARTDATESTRING="2005-06-30" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38533.59072917" TITLE="bazaar20 ad for GRIT help" TIMEESTUNITS="H" ID="49" STARTDATE="38533.00000000" POS="2">
+ <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.72346065" TITLE="bazaar20 ideas" TIMEESTUNITS="H" ID="51" STARTDATE="38583.00000000" POS="1">
+ <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.72354167" TITLE="GUI for adding/editing messages" TIMEESTUNITS="H" ID="52" STARTDATE="38583.00000000" POS="2"/>
+ <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.72365741" TITLE="XLIFF import/export" TIMEESTUNITS="H" ID="54" STARTDATE="38583.00000000" POS="1"/>
+ </TASK>
+ </TASK>
+ <TASK STARTDATESTRING="2005-06-30" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.73721065" TITLE="internal_comment for all resource nodes (not just <message>)" TIMEESTUNITS="H" ID="50" PERCENTDONE="100" STARTDATE="38533.00000000" DONEDATESTRING="2005-08-19" POS="9" DONEDATE="38583.73721065"/>
+ <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.73743056" TITLE="Preserve XML comments - this gives us line continuation and more" TIMEESTUNITS="H" ID="55" STARTDATE="38583.72326389" POS="1"/>
+</TODOLIST>
diff --git a/tools/grit/grit/grit_runner.py b/tools/grit/grit/grit_runner.py
new file mode 100644
index 0000000000..26aa0d58c4
--- /dev/null
+++ b/tools/grit/grit/grit_runner.py
@@ -0,0 +1,334 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Command processor for GRIT. This is the script you invoke to run the various
+GRIT tools.
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import getopt
+
+from grit import util
+
+import grit.extern.FP
+
+# Tool info factories; these import only within each factory to avoid
+# importing most of the GRIT code until required.
+def ToolFactoryBuild():
+ import grit.tool.build
+ return grit.tool.build.RcBuilder()
+
+def ToolFactoryBuildInfo():
+ import grit.tool.buildinfo
+ return grit.tool.buildinfo.DetermineBuildInfo()
+
+def ToolFactoryCount():
+ import grit.tool.count
+ return grit.tool.count.CountMessage()
+
+def ToolFactoryDiffStructures():
+ import grit.tool.diff_structures
+ return grit.tool.diff_structures.DiffStructures()
+
+def ToolFactoryMenuTranslationsFromParts():
+ import grit.tool.menu_from_parts
+ return grit.tool.menu_from_parts.MenuTranslationsFromParts()
+
+def ToolFactoryNewGrd():
+ import grit.tool.newgrd
+ return grit.tool.newgrd.NewGrd()
+
+def ToolFactoryResizeDialog():
+ import grit.tool.resize
+ return grit.tool.resize.ResizeDialog()
+
+def ToolFactoryRc2Grd():
+ import grit.tool.rc2grd
+ return grit.tool.rc2grd.Rc2Grd()
+
+def ToolFactoryTest():
+ import grit.tool.test
+ return grit.tool.test.TestTool()
+
+def ToolFactoryTranslationToTc():
+ import grit.tool.transl2tc
+ return grit.tool.transl2tc.TranslationToTc()
+
+def ToolFactoryUnit():
+ import grit.tool.unit
+ return grit.tool.unit.UnitTestTool()
+
+
+def ToolFactoryUpdateResourceIds():
+ import grit.tool.update_resource_ids
+ return grit.tool.update_resource_ids.UpdateResourceIds()
+
+
+def ToolFactoryXmb():
+ import grit.tool.xmb
+ return grit.tool.xmb.OutputXmb()
+
+def ToolAndroid2Grd():
+ import grit.tool.android2grd
+ return grit.tool.android2grd.Android2Grd()
+
+# Keys for the following map
+_FACTORY = 1
+_REQUIRES_INPUT = 2
+_HIDDEN = 3 # optional key - presence indicates tool is hidden
+
+# Maps tool names to the tool's module. Done as a list of (key, value) tuples
+# instead of a map to preserve ordering.
+_TOOLS = [
+ ['android2grd', {
+ _FACTORY: ToolAndroid2Grd,
+ _REQUIRES_INPUT: False
+ }],
+ ['build', {
+ _FACTORY: ToolFactoryBuild,
+ _REQUIRES_INPUT: True
+ }],
+ ['buildinfo', {
+ _FACTORY: ToolFactoryBuildInfo,
+ _REQUIRES_INPUT: True
+ }],
+ ['count', {
+ _FACTORY: ToolFactoryCount,
+ _REQUIRES_INPUT: True
+ }],
+ [
+ 'menufromparts',
+ {
+ _FACTORY: ToolFactoryMenuTranslationsFromParts,
+ _REQUIRES_INPUT: True,
+ _HIDDEN: True
+ }
+ ],
+ ['newgrd', {
+ _FACTORY: ToolFactoryNewGrd,
+ _REQUIRES_INPUT: False
+ }],
+ ['rc2grd', {
+ _FACTORY: ToolFactoryRc2Grd,
+ _REQUIRES_INPUT: False
+ }],
+ ['resize', {
+ _FACTORY: ToolFactoryResizeDialog,
+ _REQUIRES_INPUT: True
+ }],
+ ['sdiff', {
+ _FACTORY: ToolFactoryDiffStructures,
+ _REQUIRES_INPUT: False
+ }],
+ ['test', {
+ _FACTORY: ToolFactoryTest,
+ _REQUIRES_INPUT: True,
+ _HIDDEN: True
+ }],
+ [
+ 'transl2tc',
+ {
+ _FACTORY: ToolFactoryTranslationToTc,
+ _REQUIRES_INPUT: False
+ }
+ ],
+ ['unit', {
+ _FACTORY: ToolFactoryUnit,
+ _REQUIRES_INPUT: False
+ }],
+ [
+ 'update_resource_ids',
+ {
+ _FACTORY: ToolFactoryUpdateResourceIds,
+ _REQUIRES_INPUT: False
+ }
+ ],
+ ['xmb', {
+ _FACTORY: ToolFactoryXmb,
+ _REQUIRES_INPUT: True
+ }],
+]
+
+
+def PrintUsage():
+ tool_list = ''
+ for (tool, info) in _TOOLS:
+ if not _HIDDEN in info:
+ tool_list += ' %-12s %s\n' % (
+ tool, info[_FACTORY]().ShortDescription())
+
+ print("""GRIT - the Google Resource and Internationalization Tool
+
+Usage: grit [GLOBALOPTIONS] TOOL [args to tool]
+
+Global options:
+
+ -i INPUT Specifies the INPUT file to use (a .grd file). If this is not
+ specified, GRIT will look for the environment variable GRIT_INPUT.
+ If it is not present either, GRIT will try to find an input file
+ named 'resource.grd' in the current working directory.
+
+ -h MODULE Causes GRIT to use MODULE.UnsignedFingerPrint instead of
+ grit.extern.FP.UnsignedFingerprint. MODULE must be
+ available somewhere in the PYTHONPATH search path.
+
+ -v Print more verbose runtime information.
+
+ -x Print extremely verbose runtime information. Implies -v
+
+ -p FNAME Specifies that GRIT should profile its execution and output the
+ results to the file FNAME.
+
+Tools:
+
+ TOOL can be one of the following:
+%s
+ For more information on how to use a particular tool, and the specific
+ arguments you can send to that tool, execute 'grit help TOOL'
+""" % (tool_list))
+
+
+class Options(object):
+ """Option storage and parsing."""
+
+ def __init__(self):
+ self.hash = None
+ self.input = None
+ self.verbose = False
+ self.extra_verbose = False
+ self.output_stream = sys.stdout
+ self.profile_dest = None
+
+ def ReadOptions(self, args):
+ """Reads options from the start of args and returns the remainder."""
+ (opts, args) = getopt.getopt(args, 'vxi:p:h:', ('help',))
+ for (key, val) in opts:
+ if key == '-h': self.hash = val
+ elif key == '-i': self.input = val
+ elif key == '-v':
+ self.verbose = True
+ util.verbose = True
+ elif key == '-x':
+ self.verbose = True
+ util.verbose = True
+ self.extra_verbose = True
+ util.extra_verbose = True
+ elif key == '-p': self.profile_dest = val
+ elif key == '--help':
+ PrintUsage()
+ sys.exit(0)
+
+ if not self.input:
+ if 'GRIT_INPUT' in os.environ:
+ self.input = os.environ['GRIT_INPUT']
+ else:
+ self.input = 'resource.grd'
+
+ return args
+
+ def __repr__(self):
+ return '(verbose: %d, input: %s)' % (
+ self.verbose, self.input)
+
+
+def _GetToolInfo(tool):
+ """Returns the info map for the tool named 'tool' or None if there is no
+ such tool."""
+ matches = [t for t in _TOOLS if t[0] == tool]
+ if not matches:
+ return None
+ else:
+ return matches[0][1]
+
+
+def Main(args=None):
+ """Parses arguments and does the appropriate thing."""
+ util.ChangeStdoutEncoding()
+
+ # Support for setuptools console wrappers.
+ if args is None:
+ args = sys.argv[1:]
+
+ options = Options()
+ try:
+ args = options.ReadOptions(args) # args may be shorter after this
+ except getopt.GetoptError as e:
+ print("grit:", str(e))
+ print("Try running 'grit help' for valid options.")
+ return 1
+ if not args:
+ print("No tool provided. Try running 'grit help' for a list of tools.")
+ return 2
+
+ tool = args[0]
+ if tool == 'help':
+ if len(args) == 1:
+ PrintUsage()
+ return 0
+ else:
+ tool = args[1]
+ if not _GetToolInfo(tool):
+ print("No such tool. Try running 'grit help' for a list of tools.")
+ return 2
+
+ print("Help for 'grit %s' (for general help, run 'grit help'):\n" %
+ (tool,))
+ _GetToolInfo(tool)[_FACTORY]().ShowUsage()
+ return 0
+ if not _GetToolInfo(tool):
+ print("No such tool. Try running 'grit help' for a list of tools.")
+ return 2
+
+ try:
+ if _GetToolInfo(tool)[_REQUIRES_INPUT]:
+ os.stat(options.input)
+ except OSError:
+ print('Input file %s not found.\n'
+ 'To specify a different input file:\n'
+ ' 1. Use the GRIT_INPUT environment variable.\n'
+ ' 2. Use the -i command-line option. This overrides '
+ 'GRIT_INPUT.\n'
+ ' 3. Specify neither GRIT_INPUT or -i and GRIT will try to load '
+ "'resource.grd'\n"
+ ' from the current directory.' % options.input)
+ return 2
+
+ if options.hash:
+ grit.extern.FP.UseUnsignedFingerPrintFromModule(options.hash)
+
+ try:
+ toolobject = _GetToolInfo(tool)[_FACTORY]()
+ if options.profile_dest:
+ import hotshot
+ prof = hotshot.Profile(options.profile_dest)
+ return prof.runcall(toolobject.Run, options, args[1:])
+ else:
+ return toolobject.Run(options, args[1:])
+ except getopt.GetoptError as e:
+ print("grit: %s: %s" % (tool, str(e)))
+ print("Try running 'grit help %s' for valid options." % (tool,))
+ return 1
+
+
+if __name__ == '__main__':
+ sys.path.append(
+ os.path.join(
+ os.path.dirname(
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
+ 'diagnosis'))
+ try:
+ import crbug_1001171
+ with crbug_1001171.DumpStateOnLookupError():
+ sys.exit(Main(sys.argv[1:]))
+ except ImportError:
+ pass
+
+ sys.exit(Main(sys.argv[1:]))
diff --git a/tools/grit/grit/grit_runner_unittest.py b/tools/grit/grit/grit_runner_unittest.py
new file mode 100644
index 0000000000..1487001d81
--- /dev/null
+++ b/tools/grit/grit/grit_runner_unittest.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.py'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import util
+import grit.grit_runner
+
+class OptionArgsUnittest(unittest.TestCase):
+ def setUp(self):
+ self.buf = StringIO()
+ self.old_stdout = sys.stdout
+ sys.stdout = self.buf
+
+ def tearDown(self):
+ sys.stdout = self.old_stdout
+
+ def testSimple(self):
+ grit.grit_runner.Main(['-i',
+ util.PathFromRoot('grit/testdata/simple-input.xml'),
+ 'test', 'bla', 'voff', 'ga'])
+ output = self.buf.getvalue()
+ self.failUnless(output.count("'test'") == 0) # tool name doesn't occur
+ self.failUnless(output.count('bla'))
+ self.failUnless(output.count('simple-input.xml'))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/lazy_re.py b/tools/grit/grit/lazy_re.py
new file mode 100644
index 0000000000..5c461e87e7
--- /dev/null
+++ b/tools/grit/grit/lazy_re.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''In GRIT, we used to compile a lot of regular expressions at parse
+time. Since many of them never get used, we use lazy_re to compile
+them on demand the first time they are used, thus speeding up startup
+time in some cases.
+'''
+
+from __future__ import print_function
+
+import re
+
+
+class LazyRegexObject(object):
+ '''This object creates a RegexObject with the arguments passed in
+ its constructor, the first time any attribute except the several on
+ the class itself is accessed. This accomplishes lazy compilation of
+ the regular expression while maintaining a nearly-identical
+ interface.
+ '''
+
+ def __init__(self, *args, **kwargs):
+ self._stash_args = args
+ self._stash_kwargs = kwargs
+ self._lazy_re = None
+
+ def _LazyInit(self):
+ if not self._lazy_re:
+ self._lazy_re = re.compile(*self._stash_args, **self._stash_kwargs)
+
+ def __getattribute__(self, name):
+ if name in ('_LazyInit', '_lazy_re', '_stash_args', '_stash_kwargs'):
+ return object.__getattribute__(self, name)
+ else:
+ self._LazyInit()
+ return getattr(self._lazy_re, name)
+
+
+def compile(*args, **kwargs):
+ '''Creates a LazyRegexObject that, when invoked on, will compile a
+ re.RegexObject (via re.compile) with the same arguments passed to
+ this function, and delegate almost all of its methods to it.
+ '''
+ return LazyRegexObject(*args, **kwargs)
diff --git a/tools/grit/grit/lazy_re_unittest.py b/tools/grit/grit/lazy_re_unittest.py
new file mode 100644
index 0000000000..8488b454ee
--- /dev/null
+++ b/tools/grit/grit/lazy_re_unittest.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit test for lazy_re.
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import re
+import unittest
+
+from grit import lazy_re
+
+
+class LazyReUnittest(unittest.TestCase):
+
+ def testCreatedOnlyOnDemand(self):
+ rex = lazy_re.compile('bingo')
+ self.assertEqual(None, rex._lazy_re)
+ self.assertTrue(rex.match('bingo'))
+ self.assertNotEqual(None, rex._lazy_re)
+
+ def testJustKwargsWork(self):
+ rex = lazy_re.compile(flags=re.I, pattern='BiNgO')
+ self.assertTrue(rex.match('bingo'))
+
+ def testPositionalAndKwargsWork(self):
+ rex = lazy_re.compile('BiNgO', flags=re.I)
+ self.assertTrue(rex.match('bingo'))
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/node/__init__.py b/tools/grit/grit/node/__init__.py
new file mode 100644
index 0000000000..2fc0d3360c
--- /dev/null
+++ b/tools/grit/grit/node/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Package 'grit.node'
+'''
+
+pass
diff --git a/tools/grit/grit/node/base.py b/tools/grit/grit/node/base.py
new file mode 100644
index 0000000000..40859d301d
--- /dev/null
+++ b/tools/grit/grit/node/base.py
@@ -0,0 +1,670 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Base types for nodes in a GRIT resource tree.
+'''
+
+from __future__ import print_function
+
+import ast
+import os
+import struct
+import sys
+from xml.sax import saxutils
+
+import six
+
+from grit import constants
+from grit import clique
+from grit import exception
+from grit import util
+from grit.node import brotli_util
+import grit.format.gzip_string
+
+
+class Node(object):
+ '''An item in the tree that has children.'''
+
+ # Valid content types that can be returned by _ContentType()
+ _CONTENT_TYPE_NONE = 0 # No CDATA content but may have children
+ _CONTENT_TYPE_CDATA = 1 # Only CDATA, no children.
+ _CONTENT_TYPE_MIXED = 2 # CDATA and children, possibly intermingled
+
+ # Types of files to be compressed by default.
+ _COMPRESS_BY_DEFAULT_EXTENSIONS = ('.js', '.html', '.css', '.svg')
+
+ # Default nodes to not whitelist skipped
+ _whitelist_marked_as_skip = False
+
+ # A class-static cache to speed up EvaluateExpression().
+ # Keys are expressions (e.g. 'is_ios and lang == "fr"'). Values are tuples
+ # (code, variables_in_expr) where code is the compiled expression and can be
+ # directly eval'd, and variables_in_expr is the list of variable and method
+ # names used in the expression (e.g. ['is_ios', 'lang']).
+ eval_expr_cache = {}
+
+ def __init__(self):
+ self.children = [] # A list of child elements
+ self.mixed_content = [] # A list of u'' and/or child elements (this
+ # duplicates 'children' but
+ # is needed to preserve markup-type content).
+ self.name = u'' # The name of this element
+ self.attrs = {} # The set of attributes (keys to values)
+ self.parent = None # Our parent unless we are the root element.
+ self.uberclique = None # Allows overriding uberclique for parts of tree
+ self.source = None # File that this node was parsed from
+
+ # This context handler allows you to write "with node:" and get a
+ # line identifying the offending node if an exception escapes from the body
+ # of the with statement.
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if exc_type is not None:
+ print(u'Error processing node %s: %s' % (six.text_type(self), exc_value))
+
+ def __iter__(self):
+ '''A preorder iteration through the tree that this node is the root of.'''
+ return self.Preorder()
+
+ def Preorder(self):
+ '''Generator that generates first this node, then the same generator for
+ any child nodes.'''
+ yield self
+ for child in self.children:
+ for iterchild in child.Preorder():
+ yield iterchild
+
+ def ActiveChildren(self):
+ '''Returns the children of this node that should be included in the current
+ configuration. Overridden by <if>.'''
+ return [node for node in self.children if not node.WhitelistMarkedAsSkip()]
+
+ def ActiveDescendants(self):
+ '''Yields the current node and all descendants that should be included in
+ the current configuration, in preorder.'''
+ yield self
+ for child in self.ActiveChildren():
+ for descendant in child.ActiveDescendants():
+ yield descendant
+
+ def GetRoot(self):
+ '''Returns the root Node in the tree this Node belongs to.'''
+ curr = self
+ while curr.parent:
+ curr = curr.parent
+ return curr
+
+ # TODO(joi) Use this (currently untested) optimization?:
+ #if hasattr(self, '_root'):
+ # return self._root
+ #curr = self
+ #while curr.parent and not hasattr(curr, '_root'):
+ # curr = curr.parent
+ #if curr.parent:
+ # self._root = curr._root
+ #else:
+ # self._root = curr
+ #return self._root
+
+ def StartParsing(self, name, parent):
+ '''Called at the start of parsing.
+
+ Args:
+ name: u'elementname'
+ parent: grit.node.base.Node or subclass or None
+ '''
+ assert isinstance(name, six.string_types)
+ assert not parent or isinstance(parent, Node)
+ self.name = name
+ self.parent = parent
+
+ def AddChild(self, child):
+ '''Adds a child to the list of children of this node, if it is a valid
+ child for the node.'''
+ assert isinstance(child, Node)
+ if (not self._IsValidChild(child) or
+ self._ContentType() == self._CONTENT_TYPE_CDATA):
+ explanation = 'invalid child %s for parent %s' % (str(child), self.name)
+ raise exception.UnexpectedChild(explanation)
+ self.children.append(child)
+ self.mixed_content.append(child)
+
+ def RemoveChild(self, child_id):
+ '''Removes the first node that has a "name" attribute which
+ matches "child_id" in the list of immediate children of
+ this node.
+
+ Args:
+ child_id: String identifying the child to be removed
+ '''
+ index = 0
+ # Safe not to copy since we only remove the first element found
+ for child in self.children:
+ name_attr = child.attrs['name']
+ if name_attr == child_id:
+ self.children.pop(index)
+ self.mixed_content.pop(index)
+ break
+ index += 1
+
+ def AppendContent(self, content):
+ '''Appends a chunk of text as content of this node.
+
+ Args:
+ content: u'hello'
+
+ Return:
+ None
+ '''
+ assert isinstance(content, six.string_types)
+ if self._ContentType() != self._CONTENT_TYPE_NONE:
+ self.mixed_content.append(content)
+ elif content.strip() != '':
+ raise exception.UnexpectedContent()
+
+ def HandleAttribute(self, attrib, value):
+ '''Informs the node of an attribute that was parsed out of the GRD file
+ for it.
+
+ Args:
+ attrib: 'name'
+ value: 'fooblat'
+
+ Return:
+ None
+ '''
+ assert isinstance(attrib, six.string_types)
+ assert isinstance(value, six.string_types)
+ if self._IsValidAttribute(attrib, value):
+ self.attrs[attrib] = value
+ else:
+ raise exception.UnexpectedAttribute(attrib)
+
+ def EndParsing(self):
+ '''Called at the end of parsing.'''
+
+ # TODO(joi) Rewrite this, it's extremely ugly!
+ if len(self.mixed_content):
+ if isinstance(self.mixed_content[0], six.string_types):
+ # Remove leading and trailing chunks of pure whitespace.
+ while (len(self.mixed_content) and
+ isinstance(self.mixed_content[0], six.string_types) and
+ self.mixed_content[0].strip() == ''):
+ self.mixed_content = self.mixed_content[1:]
+ # Strip leading and trailing whitespace from mixed content chunks
+ # at front and back.
+ if (len(self.mixed_content) and
+ isinstance(self.mixed_content[0], six.string_types)):
+ self.mixed_content[0] = self.mixed_content[0].lstrip()
+ # Remove leading and trailing ''' (used to demarcate whitespace)
+ if (len(self.mixed_content) and
+ isinstance(self.mixed_content[0], six.string_types)):
+ if self.mixed_content[0].startswith("'''"):
+ self.mixed_content[0] = self.mixed_content[0][3:]
+ if len(self.mixed_content):
+ if isinstance(self.mixed_content[-1], six.string_types):
+ # Same stuff all over again for the tail end.
+ while (len(self.mixed_content) and
+ isinstance(self.mixed_content[-1], six.string_types) and
+ self.mixed_content[-1].strip() == ''):
+ self.mixed_content = self.mixed_content[:-1]
+ if (len(self.mixed_content) and
+ isinstance(self.mixed_content[-1], six.string_types)):
+ self.mixed_content[-1] = self.mixed_content[-1].rstrip()
+ if (len(self.mixed_content) and
+ isinstance(self.mixed_content[-1], six.string_types)):
+ if self.mixed_content[-1].endswith("'''"):
+ self.mixed_content[-1] = self.mixed_content[-1][:-3]
+
+ # Check that all mandatory attributes are there.
+ for node_mandatt in self.MandatoryAttributes():
+ mandatt_list = []
+ if node_mandatt.find('|') >= 0:
+ mandatt_list = node_mandatt.split('|')
+ else:
+ mandatt_list.append(node_mandatt)
+
+ mandatt_option_found = False
+ for mandatt in mandatt_list:
+ assert mandatt not in self.DefaultAttributes()
+ if mandatt in self.attrs:
+ if not mandatt_option_found:
+ mandatt_option_found = True
+ else:
+ raise exception.MutuallyExclusiveMandatoryAttribute(mandatt)
+
+ if not mandatt_option_found:
+ raise exception.MissingMandatoryAttribute(mandatt)
+
+ # Add default attributes if not specified in input file.
+ for defattr in self.DefaultAttributes():
+ if not defattr in self.attrs:
+ self.attrs[defattr] = self.DefaultAttributes()[defattr]
+
+ def GetCdata(self):
+ '''Returns all CDATA of this element, concatenated into a single
+ string. Note that this ignores any elements embedded in CDATA.'''
+ return ''.join([c for c in self.mixed_content
+ if isinstance(c, six.string_types)])
+
+ def __str__(self):
+ '''Returns this node and all nodes below it as an XML document in a Unicode
+ string.'''
+ header = u'<?xml version="1.0" encoding="UTF-8"?>\n'
+ return header + self.FormatXml()
+
+ # Some Python 2 glue.
+ __unicode__ = __str__
+
+ def FormatXml(self, indent = u'', one_line = False):
+ '''Returns this node and all nodes below it as an XML
+ element in a Unicode string. This differs from __unicode__ in that it does
+ not include the <?xml> stuff at the top of the string. If one_line is true,
+ children and CDATA are layed out in a way that preserves internal
+ whitespace.
+ '''
+ assert isinstance(indent, six.string_types)
+
+ content_one_line = (one_line or
+ self._ContentType() == self._CONTENT_TYPE_MIXED)
+ inside_content = self.ContentsAsXml(indent, content_one_line)
+
+ # Then the attributes for this node.
+ attribs = u''
+ default_attribs = self.DefaultAttributes()
+ for attrib, value in sorted(self.attrs.items()):
+ # Only print an attribute if it is other than the default value.
+ if attrib not in default_attribs or value != default_attribs[attrib]:
+ attribs += u' %s=%s' % (attrib, saxutils.quoteattr(value))
+
+ # Finally build the XML for our node and return it
+ if len(inside_content) > 0:
+ if one_line:
+ return u'<%s%s>%s</%s>' % (self.name, attribs, inside_content,
+ self.name)
+ elif content_one_line:
+ return u'%s<%s%s>\n%s %s\n%s</%s>' % (
+ indent, self.name, attribs,
+ indent, inside_content,
+ indent, self.name)
+ else:
+ return u'%s<%s%s>\n%s\n%s</%s>' % (
+ indent, self.name, attribs,
+ inside_content,
+ indent, self.name)
+ else:
+ return u'%s<%s%s />' % (indent, self.name, attribs)
+
+ def ContentsAsXml(self, indent, one_line):
+ '''Returns the contents of this node (CDATA and child elements) in XML
+ format. If 'one_line' is true, the content will be laid out on one line.'''
+ assert isinstance(indent, six.string_types)
+
+ # Build the contents of the element.
+ inside_parts = []
+ last_item = None
+ for mixed_item in self.mixed_content:
+ if isinstance(mixed_item, Node):
+ inside_parts.append(mixed_item.FormatXml(indent + u' ', one_line))
+ if not one_line:
+ inside_parts.append(u'\n')
+ else:
+ message = mixed_item
+ # If this is the first item and it starts with whitespace, we add
+ # the ''' delimiter.
+ if not last_item and message.lstrip() != message:
+ message = u"'''" + message
+ inside_parts.append(util.EncodeCdata(message))
+ last_item = mixed_item
+
+ # If there are only child nodes and no cdata, there will be a spurious
+ # trailing \n
+ if len(inside_parts) and inside_parts[-1] == '\n':
+ inside_parts = inside_parts[:-1]
+
+ # If the last item is a string (not a node) and ends with whitespace,
+ # we need to add the ''' delimiter.
+ if (isinstance(last_item, six.string_types) and
+ last_item.rstrip() != last_item):
+ inside_parts[-1] = inside_parts[-1] + u"'''"
+
+ return u''.join(inside_parts)
+
+ def SubstituteMessages(self, substituter):
+ '''Applies substitutions to all messages in the tree.
+
+ Called as a final step of RunGatherers.
+
+ Args:
+ substituter: a grit.util.Substituter object.
+ '''
+ for child in self.children:
+ child.SubstituteMessages(substituter)
+
+ def _IsValidChild(self, child):
+ '''Returns true if 'child' is a valid child of this node.
+ Overridden by subclasses.'''
+ return False
+
+ def _IsValidAttribute(self, name, value):
+ '''Returns true if 'name' is the name of a valid attribute of this element
+ and 'value' is a valid value for that attribute. Overriden by
+ subclasses unless they have only mandatory attributes.'''
+ return (name in self.MandatoryAttributes() or
+ name in self.DefaultAttributes())
+
+ def _ContentType(self):
+ '''Returns the type of content this element can have. Overridden by
+ subclasses. The content type can be one of the _CONTENT_TYPE_XXX constants
+ above.'''
+ return self._CONTENT_TYPE_NONE
+
+ def MandatoryAttributes(self):
+ '''Returns a list of attribute names that are mandatory (non-optional)
+ on the current element. One can specify a list of
+ "mutually exclusive mandatory" attributes by specifying them as one
+ element in the list, separated by a "|" character.
+ '''
+ return []
+
+ def DefaultAttributes(self):
+ '''Returns a dictionary of attribute names that have defaults, mapped to
+ the default value. Overridden by subclasses.'''
+ return {}
+
+ def GetCliques(self):
+ '''Returns all MessageClique objects belonging to this node. Overridden
+ by subclasses.
+
+ Return:
+ [clique1, clique2] or []
+ '''
+ return []
+
+ def ToRealPath(self, path_from_basedir):
+ '''Returns a real path (which can be absolute or relative to the current
+ working directory), given a path that is relative to the base directory
+ set for the GRIT input file.
+
+ Args:
+ path_from_basedir: '..'
+
+ Return:
+ 'resource'
+ '''
+ return util.normpath(os.path.join(self.GetRoot().GetBaseDir(),
+ os.path.expandvars(path_from_basedir)))
+
+ def GetInputPath(self):
+ '''Returns a path, relative to the base directory set for the grd file,
+ that points to the file the node refers to.
+ '''
+ # This implementation works for most nodes that have an input file.
+ return self.attrs['file']
+
+ def UberClique(self):
+ '''Returns the uberclique that should be used for messages originating in
+ a given node. If the node itself has its uberclique set, that is what we
+ use, otherwise we search upwards until we find one. If we do not find one
+ even at the root node, we set the root node's uberclique to a new
+ uberclique instance.
+ '''
+ node = self
+ while not node.uberclique and node.parent:
+ node = node.parent
+ if not node.uberclique:
+ node.uberclique = clique.UberClique()
+ return node.uberclique
+
+ def IsTranslateable(self):
+ '''Returns false if the node has contents that should not be translated,
+ otherwise returns false (even if the node has no contents).
+ '''
+ if not 'translateable' in self.attrs:
+ return True
+ else:
+ return self.attrs['translateable'] == 'true'
+
+ def IsAccessibilityWithNoUI(self):
+ '''Returns true if the node is marked as an accessibility label and the
+ message isn't shown in the UI. Otherwise returns false. This label is
+ used to determine if the text requires screenshots.'''
+ if not 'is_accessibility_with_no_ui' in self.attrs:
+ return False
+ else:
+ return self.attrs['is_accessibility_with_no_ui'] == 'true'
+
+ def GetNodeById(self, id):
+ '''Returns the node in the subtree parented by this node that has a 'name'
+ attribute matching 'id'. Returns None if no such node is found.
+ '''
+ for node in self:
+ if 'name' in node.attrs and node.attrs['name'] == id:
+ return node
+ return None
+
+ def GetChildrenOfType(self, type):
+ '''Returns a list of all subnodes (recursing to all leaves) of this node
+ that are of the indicated type (or tuple of types).
+
+ Args:
+ type: A type you could use with isinstance().
+
+ Return:
+ A list, possibly empty.
+ '''
+ return [child for child in self if isinstance(child, type)]
+
+ def GetTextualIds(self):
+ '''Returns a list of the textual ids of this node.
+ '''
+ if 'name' in self.attrs:
+ return [self.attrs['name']]
+ return []
+
+ @classmethod
+ def EvaluateExpression(cls, expr, defs, target_platform, extra_variables={}):
+ '''Worker for EvaluateCondition (below) and conditions in XTB files.'''
+ if expr in cls.eval_expr_cache:
+ code, variables_in_expr = cls.eval_expr_cache[expr]
+ else:
+ # Get a list of all variable and method names used in the expression.
+ syntax_tree = ast.parse(expr, mode='eval')
+ variables_in_expr = [node.id for node in ast.walk(syntax_tree) if
+ isinstance(node, ast.Name) and node.id not in ('True', 'False')]
+ code = compile(syntax_tree, filename='<string>', mode='eval')
+ cls.eval_expr_cache[expr] = code, variables_in_expr
+
+ # Set values only for variables that are needed to eval the expression.
+ variable_map = {}
+ for name in variables_in_expr:
+ if name == 'os':
+ value = target_platform
+ elif name == 'defs':
+ value = defs
+
+ elif name == 'is_linux':
+ value = target_platform.startswith('linux')
+ elif name == 'is_macosx':
+ value = target_platform == 'darwin'
+ elif name == 'is_win':
+ value = target_platform in ('cygwin', 'win32')
+ elif name == 'is_android':
+ value = target_platform == 'android'
+ elif name == 'is_ios':
+ value = target_platform == 'ios'
+ elif name == 'is_bsd':
+ value = 'bsd' in target_platform
+ elif name == 'is_posix':
+ value = (target_platform in ('darwin', 'linux2', 'linux3', 'sunos5',
+ 'android', 'ios')
+ or 'bsd' in target_platform)
+
+ elif name == 'pp_ifdef':
+ def pp_ifdef(symbol):
+ return symbol in defs
+ value = pp_ifdef
+ elif name == 'pp_if':
+ def pp_if(symbol):
+ return defs.get(symbol, False)
+ value = pp_if
+
+ elif name in defs:
+ value = defs[name]
+ elif name in extra_variables:
+ value = extra_variables[name]
+ else:
+ # Undefined variables default to False.
+ value = False
+
+ variable_map[name] = value
+
+ eval_result = eval(code, {}, variable_map)
+ assert isinstance(eval_result, bool)
+ return eval_result
+
+ def EvaluateCondition(self, expr):
+ '''Returns true if and only if the Python expression 'expr' evaluates
+ to true.
+
+ The expression is given a few local variables:
+ - 'lang' is the language currently being output
+ (the 'lang' attribute of the <output> element).
+ - 'context' is the current output context
+ (the 'context' attribute of the <output> element).
+ - 'defs' is a map of C preprocessor-style symbol names to their values.
+ - 'os' is the current platform (likely 'linux2', 'win32' or 'darwin').
+ - 'pp_ifdef(symbol)' is a shorthand for "symbol in defs".
+ - 'pp_if(symbol)' is a shorthand for "symbol in defs and defs[symbol]".
+ - 'is_linux', 'is_macosx', 'is_win', 'is_posix' are true if 'os'
+ matches the given platform.
+ '''
+ root = self.GetRoot()
+ lang = getattr(root, 'output_language', '')
+ context = getattr(root, 'output_context', '')
+ defs = getattr(root, 'defines', {})
+ target_platform = getattr(root, 'target_platform', '')
+ extra_variables = {
+ 'lang': lang,
+ 'context': context,
+ }
+ return Node.EvaluateExpression(
+ expr, defs, target_platform, extra_variables)
+
+ def OnlyTheseTranslations(self, languages):
+ '''Turns off loading of translations for languages not in the provided list.
+
+ Attrs:
+ languages: ['fr', 'zh_cn']
+ '''
+ for node in self:
+ if (hasattr(node, 'IsTranslation') and
+ node.IsTranslation() and
+ node.GetLang() not in languages):
+ node.DisableLoading()
+
+ def FindBooleanAttribute(self, attr, default, skip_self):
+ '''Searches all ancestors of the current node for the nearest enclosing
+ definition of the given boolean attribute.
+
+ Args:
+ attr: 'fallback_to_english'
+ default: What to return if no node defines the attribute.
+ skip_self: Don't check the current node, only its parents.
+ '''
+ p = self.parent if skip_self else self
+ while p:
+ value = p.attrs.get(attr, 'default').lower()
+ if value != 'default':
+ return (value == 'true')
+ p = p.parent
+ return default
+
+ def PseudoIsAllowed(self):
+ '''Returns true if this node is allowed to use pseudo-translations. This
+ is true by default, unless this node is within a <release> node that has
+ the allow_pseudo attribute set to false.
+ '''
+ return self.FindBooleanAttribute('allow_pseudo',
+ default=True, skip_self=True)
+
+ def ShouldFallbackToEnglish(self):
+ '''Returns true iff this node should fall back to English when
+ pseudotranslations are disabled and no translation is available for a
+ given message.
+ '''
+ return self.FindBooleanAttribute('fallback_to_english',
+ default=False, skip_self=True)
+
+ def WhitelistMarkedAsSkip(self):
+ '''Returns true if the node is marked to be skipped in the output by a
+ whitelist.
+ '''
+ return self._whitelist_marked_as_skip
+
+ def SetWhitelistMarkedAsSkip(self, mark_skipped):
+ '''Sets WhitelistMarkedAsSkip.
+ '''
+ self._whitelist_marked_as_skip = mark_skipped
+
+ def ExpandVariables(self):
+ '''Whether we need to expand variables on a given node.'''
+ return False
+
+ def IsResourceMapSource(self):
+ '''Whether this node is a resource map source.'''
+ return False
+
+ def CompressDataIfNeeded(self, data):
+ '''Compress data using the format specified in the compress attribute.
+
+ Args:
+ data: The data to compressed.
+ Returns:
+ The data in gzipped or brotli compressed format. If the format is
+ unspecified then this returns the data uncompressed.
+ '''
+
+ compress = self.attrs.get('compress')
+
+ # Compress JS, HTML, CSS and SVG files by default (gzip), unless |compress|
+ # is explicitly specified.
+ compress_by_default = (compress == 'default'
+ and self.attrs.get('file').endswith(
+ self._COMPRESS_BY_DEFAULT_EXTENSIONS))
+
+ if compress == 'gzip' or compress_by_default:
+ # We only use rsyncable compression on Linux.
+ # We exclude ChromeOS since ChromeOS bots are Linux based but do not have
+ # the --rsyncable option built in for gzip. See crbug.com/617950.
+ if sys.platform == 'linux2' and 'chromeos' not in self.GetRoot().defines:
+ return grit.format.gzip_string.GzipStringRsyncable(data)
+ return grit.format.gzip_string.GzipString(data)
+
+ if compress == 'brotli':
+ # The length of the uncompressed data as 8 bytes little-endian.
+ size_bytes = struct.pack("<q", len(data))
+ data = brotli_util.BrotliCompress(data)
+ # BROTLI_CONST is prepended to brotli decompressed data in order to
+ # easily check if a resource has been brotli compressed.
+ # The length of the uncompressed data is also appended to the start,
+ # truncated to 6 bytes, little-endian. size_bytes is 8 bytes,
+ # need to truncate further to 6.
+ formatter = b'%ds %dx %ds' % (6, 2, len(size_bytes) - 8)
+ return (constants.BROTLI_CONST +
+ b''.join(struct.unpack(formatter, size_bytes)) +
+ data)
+
+ if compress == 'false' or compress == 'default':
+ return data
+
+ raise Exception('Invalid value for compression')
+
+
+class ContentNode(Node):
+ '''Convenience baseclass for nodes that can have content.'''
+ def _ContentType(self):
+ return self._CONTENT_TYPE_MIXED
diff --git a/tools/grit/grit/node/base_unittest.py b/tools/grit/grit/node/base_unittest.py
new file mode 100644
index 0000000000..32a5a0ca59
--- /dev/null
+++ b/tools/grit/grit/node/base_unittest.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for base.Node functionality (as used in various subclasses)'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+ sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit import util
+from grit.node import base
+from grit.node import message
+
+
+def MakePlaceholder(phname='BINGO'):
+ ph = message.PhNode()
+ ph.StartParsing(u'ph', None)
+ ph.HandleAttribute(u'name', phname)
+ ph.AppendContent(u'bongo')
+ ph.EndParsing()
+ return ph
+
+
+class NodeUnittest(unittest.TestCase):
+ def testWhitespaceHandling(self):
+ # We test using the Message node type.
+ node = message.MessageNode()
+ node.StartParsing(u'hello', None)
+ node.HandleAttribute(u'name', u'bla')
+ node.AppendContent(u" ''' two spaces ")
+ node.EndParsing()
+ self.failUnless(node.GetCdata() == u' two spaces')
+
+ node = message.MessageNode()
+ node.StartParsing(u'message', None)
+ node.HandleAttribute(u'name', u'bla')
+ node.AppendContent(u" two spaces ''' ")
+ node.EndParsing()
+ self.failUnless(node.GetCdata() == u'two spaces ')
+
+ def testWhitespaceHandlingWithChildren(self):
+ # We test using the Message node type.
+ node = message.MessageNode()
+ node.StartParsing(u'message', None)
+ node.HandleAttribute(u'name', u'bla')
+ node.AppendContent(u" ''' two spaces ")
+ node.AddChild(MakePlaceholder())
+ node.AppendContent(u' space before and after ')
+ node.AddChild(MakePlaceholder('BONGO'))
+ node.AppendContent(u" space before two after '''")
+ node.EndParsing()
+ self.failUnless(node.mixed_content[0] == u' two spaces ')
+ self.failUnless(node.mixed_content[2] == u' space before and after ')
+ self.failUnless(node.mixed_content[-1] == u' space before two after ')
+
+ def testXmlFormatMixedContent(self):
+ # Again test using the Message node type, because it is the only mixed
+ # content node.
+ node = message.MessageNode()
+ node.StartParsing(u'message', None)
+ node.HandleAttribute(u'name', u'name')
+ node.AppendContent(u'Hello <young> ')
+
+ ph = message.PhNode()
+ ph.StartParsing(u'ph', None)
+ ph.HandleAttribute(u'name', u'USERNAME')
+ ph.AppendContent(u'$1')
+ ex = message.ExNode()
+ ex.StartParsing(u'ex', None)
+ ex.AppendContent(u'Joi')
+ ex.EndParsing()
+ ph.AddChild(ex)
+ ph.EndParsing()
+
+ node.AddChild(ph)
+ node.EndParsing()
+
+ non_indented_xml = node.FormatXml()
+ self.failUnless(non_indented_xml == u'<message name="name">\n Hello '
+ u'<young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+ u'\n</message>')
+
+ indented_xml = node.FormatXml(u' ')
+ self.failUnless(indented_xml == u' <message name="name">\n Hello '
+ u'<young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+ u'\n </message>')
+
+ def testXmlFormatMixedContentWithLeadingWhitespace(self):
+ # Again test using the Message node type, because it is the only mixed
+ # content node.
+ node = message.MessageNode()
+ node.StartParsing(u'message', None)
+ node.HandleAttribute(u'name', u'name')
+ node.AppendContent(u"''' Hello <young> ")
+
+ ph = message.PhNode()
+ ph.StartParsing(u'ph', None)
+ ph.HandleAttribute(u'name', u'USERNAME')
+ ph.AppendContent(u'$1')
+ ex = message.ExNode()
+ ex.StartParsing(u'ex', None)
+ ex.AppendContent(u'Joi')
+ ex.EndParsing()
+ ph.AddChild(ex)
+ ph.EndParsing()
+
+ node.AddChild(ph)
+ node.AppendContent(u" yessiree '''")
+ node.EndParsing()
+
+ non_indented_xml = node.FormatXml()
+ self.failUnless(non_indented_xml ==
+ u"<message name=\"name\">\n ''' Hello"
+ u' <young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+ u" yessiree '''\n</message>")
+
+ indented_xml = node.FormatXml(u' ')
+ self.failUnless(indented_xml ==
+ u" <message name=\"name\">\n ''' Hello"
+ u' <young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+ u" yessiree '''\n </message>")
+
+ self.failUnless(node.GetNodeById('name'))
+
+ def testXmlFormatContentWithEntities(self):
+ '''Tests a bug where would not be escaped correctly.'''
+ from grit import tclib
+ msg_node = message.MessageNode.Construct(None, tclib.Message(
+ text = 'BEGIN_BOLDHelloWHITESPACEthere!END_BOLD Bingo!',
+ placeholders = [
+ tclib.Placeholder('BEGIN_BOLD', '<b>', 'bla'),
+ tclib.Placeholder('WHITESPACE', ' ', 'bla'),
+ tclib.Placeholder('END_BOLD', '</b>', 'bla')]),
+ 'BINGOBONGO')
+ xml = msg_node.FormatXml()
+ self.failUnless(xml.find(' ') == -1, 'should have no entities')
+
+ def testIter(self):
+ # First build a little tree of message and ph nodes.
+ node = message.MessageNode()
+ node.StartParsing(u'message', None)
+ node.HandleAttribute(u'name', u'bla')
+ node.AppendContent(u" ''' two spaces ")
+ node.AppendContent(u' space before and after ')
+ ph = message.PhNode()
+ ph.StartParsing(u'ph', None)
+ ph.AddChild(message.ExNode())
+ ph.HandleAttribute(u'name', u'BINGO')
+ ph.AppendContent(u'bongo')
+ node.AddChild(ph)
+ node.AddChild(message.PhNode())
+ node.AppendContent(u" space before two after '''")
+
+ order = [message.MessageNode, message.PhNode, message.ExNode, message.PhNode]
+ for n in node:
+ self.failUnless(type(n) == order[0])
+ order = order[1:]
+ self.failUnless(len(order) == 0)
+
+ def testGetChildrenOfType(self):
+ xml = '''<?xml version="1.0" encoding="UTF-8"?>
+ <grit latest_public_release="2" source_lang_id="en-US"
+ current_release="3" base_dir=".">
+ <outputs>
+ <output filename="resource.h" type="rc_header" />
+ <output filename="en/generated_resources.rc" type="rc_all"
+ lang="en" />
+ <if expr="pp_if('NOT_TRUE')">
+ <output filename="de/generated_resources.rc" type="rc_all"
+ lang="de" />
+ </if>
+ </outputs>
+ <release seq="3">
+ <messages>
+ <message name="ID_HELLO">Hello!</message>
+ </messages>
+ </release>
+ </grit>'''
+ grd = grd_reader.Parse(StringIO(xml),
+ util.PathFromRoot('grit/test/data'))
+ from grit.node import node_io
+ output_nodes = grd.GetChildrenOfType(node_io.OutputNode)
+ self.failUnlessEqual(len(output_nodes), 3)
+ self.failUnlessEqual(output_nodes[2].attrs['filename'],
+ 'de/generated_resources.rc')
+
+ def testEvaluateExpression(self):
+ def AssertExpr(expected_value, expr, defs, target_platform,
+ extra_variables):
+ self.failUnlessEqual(expected_value, base.Node.EvaluateExpression(
+ expr, defs, target_platform, extra_variables))
+
+ AssertExpr(True, "True", {}, 'linux', {})
+ AssertExpr(False, "False", {}, 'linux', {})
+ AssertExpr(True, "True or False", {}, 'linux', {})
+ AssertExpr(False, "True and False", {}, 'linux', {})
+ AssertExpr(True, "os == 'linux'", {}, 'linux', {})
+ AssertExpr(False, "os == 'linux'", {}, 'ios', {})
+ AssertExpr(True, "'foo' in defs", {'foo': 'bar'}, 'ios', {})
+ AssertExpr(False, "'foo' in defs", {'baz': 'bar'}, 'ios', {})
+ AssertExpr(False, "'foo' in defs", {}, 'ios', {})
+ AssertExpr(True, "is_linux", {}, 'linux2', {})
+ AssertExpr(False, "is_linux", {}, 'win32', {})
+ AssertExpr(True, "is_macosx", {}, 'darwin', {})
+ AssertExpr(False, "is_macosx", {}, 'ios', {})
+ AssertExpr(True, "is_win", {}, 'win32', {})
+ AssertExpr(False, "is_win", {}, 'darwin', {})
+ AssertExpr(True, "is_android", {}, 'android', {})
+ AssertExpr(False, "is_android", {}, 'linux3', {})
+ AssertExpr(True, "is_ios", {}, 'ios', {})
+ AssertExpr(False, "is_ios", {}, 'darwin', {})
+ AssertExpr(True, "is_posix", {}, 'linux2', {})
+ AssertExpr(True, "is_posix", {}, 'darwin', {})
+ AssertExpr(True, "is_posix", {}, 'android', {})
+ AssertExpr(True, "is_posix", {}, 'ios', {})
+ AssertExpr(True, "is_posix", {}, 'freebsd7', {})
+ AssertExpr(False, "is_posix", {}, 'win32', {})
+ AssertExpr(True, "pp_ifdef('foo')", {'foo': True}, 'win32', {})
+ AssertExpr(True, "pp_ifdef('foo')", {'foo': False}, 'win32', {})
+ AssertExpr(False, "pp_ifdef('foo')", {'bar': True}, 'win32', {})
+ AssertExpr(True, "pp_if('foo')", {'foo': True}, 'win32', {})
+ AssertExpr(False, "pp_if('foo')", {'foo': False}, 'win32', {})
+ AssertExpr(False, "pp_if('foo')", {'bar': True}, 'win32', {})
+ AssertExpr(True, "foo", {'foo': True}, 'win32', {})
+ AssertExpr(False, "foo", {'foo': False}, 'win32', {})
+ AssertExpr(False, "foo", {'bar': True}, 'win32', {})
+ AssertExpr(True, "foo == 'baz'", {'foo': 'baz'}, 'win32', {})
+ AssertExpr(False, "foo == 'baz'", {'foo': True}, 'win32', {})
+ AssertExpr(False, "foo == 'baz'", {}, 'win32', {})
+ AssertExpr(True, "lang == 'de'", {}, 'win32', {'lang': 'de'})
+ AssertExpr(False, "lang == 'de'", {}, 'win32', {'lang': 'fr'})
+ AssertExpr(False, "lang == 'de'", {}, 'win32', {})
+
+ # Test a couple more complex expressions for good measure.
+ AssertExpr(True, "is_ios and (lang in ['de', 'fr'] or foo)",
+ {'foo': 'bar'}, 'ios', {'lang': 'fr', 'context': 'today'})
+ AssertExpr(False, "is_ios and (lang in ['de', 'fr'] or foo)",
+ {'foo': False}, 'linux2', {'lang': 'fr', 'context': 'today'})
+ AssertExpr(False, "is_ios and (lang in ['de', 'fr'] or foo)",
+ {'baz': 'bar'}, 'ios', {'lang': 'he', 'context': 'today'})
+ AssertExpr(True, "foo == 'bar' or not baz",
+ {'foo': 'bar', 'fun': True}, 'ios', {'lang': 'en'})
+ AssertExpr(True, "foo == 'bar' or not baz",
+ {}, 'ios', {'lang': 'en', 'context': 'java'})
+ AssertExpr(False, "foo == 'bar' or not baz",
+ {'foo': 'ruz', 'baz': True}, 'ios', {'lang': 'en'})
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tools/grit/grit/node/brotli_util.py b/tools/grit/grit/node/brotli_util.py
new file mode 100644
index 0000000000..77f70e49d5
--- /dev/null
+++ b/tools/grit/grit/node/brotli_util.py
@@ -0,0 +1,29 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Framework for compressing resources using Brotli."""
+
+import subprocess
+
+__brotli_executable = None
+
+
+def SetBrotliCommand(brotli):
+ # brotli is a list. In production it contains the path to the Brotli executable.
+ # During testing it contains [python, mock_brotli.py] for testing on Windows.
+ global __brotli_executable
+ __brotli_executable = brotli
+
+
+def BrotliCompress(data):
+ if not __brotli_executable:
+ raise Exception('Add "use_brotli = true" to you GN grit(...) target ' +
+ 'if you want to use brotli.')
+ compress = subprocess.Popen(__brotli_executable + ['-', '-f'],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ return compress.communicate(data)[0]
+
+def IsInitialized():
+ global __brotli_executable
+ return __brotli_executable is not None
diff --git a/tools/grit/grit/node/custom/__init__.py b/tools/grit/grit/node/custom/__init__.py
new file mode 100644
index 0000000000..e179cf7730
--- /dev/null
+++ b/tools/grit/grit/node/custom/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Package 'grit.node.custom'
+'''
+
+pass
diff --git a/tools/grit/grit/node/custom/filename.py b/tools/grit/grit/node/custom/filename.py
new file mode 100644
index 0000000000..55a27e58c1
--- /dev/null
+++ b/tools/grit/grit/node/custom/filename.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A CustomType for filenames.'''
+
+from __future__ import print_function
+
+from grit import clique
+from grit import lazy_re
+
+
+class WindowsFilename(clique.CustomType):
+ '''Validates that messages can be used as Windows filenames, and strips
+ illegal characters out of translations.
+ '''
+
+ BANNED = lazy_re.compile(r'\+|:|\/|\\\\|\*|\?|\"|\<|\>|\|')
+
+ def Validate(self, message):
+ return not self.BANNED.search(message.GetPresentableContent())
+
+ def ValidateAndModify(self, lang, translation):
+ is_ok = self.Validate(translation)
+ self.ModifyEachTextPart(lang, translation)
+ return is_ok
+
+ def ModifyTextPart(self, lang, text):
+ return self.BANNED.sub(' ', text)
diff --git a/tools/grit/grit/node/custom/filename_unittest.py b/tools/grit/grit/node/custom/filename_unittest.py
new file mode 100644
index 0000000000..8e2a6dd64a
--- /dev/null
+++ b/tools/grit/grit/node/custom/filename_unittest.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
--> --------------------
--> maximum size reached
--> --------------------
[Verzeichnis aufwärts0.932unsichere VerbindungÜbersetzung europäischer Sprachen durch Browser2026-04-26]
|
2026-05-26
|
|
|
|
|