From 3dbdb83c0fee5863171e20174ded8a8b74d16e9e Mon Sep 17 00:00:00 2001 From: Nico Weber Date: Thu, 12 Nov 2015 11:05:30 -0800 Subject: Move grit from DEPS into src. This copies the currently DEPS'd in revision, r201. No intended behavior change. BUG=553682 R=primiano@chromium.org TBR=mnaganov Review URL: https://codereview.chromium.org/1410853008 . Cr-Commit-Position: refs/heads/master@{#359352} --- .gitignore | 1 - DEPS | 3 - .../third_party_files_whitelist.txt | 3 + tools/grit/.gitignore | 1 + tools/grit/LICENSE | 25 + tools/grit/PRESUBMIT.py | 22 + tools/grit/README | 2 + tools/grit/codereview.settings | 6 + tools/grit/grit.py | 16 + tools/grit/grit/__init__.py | 10 + tools/grit/grit/clique.py | 483 +++ tools/grit/grit/clique_unittest.py | 261 ++ tools/grit/grit/constants.py | 18 + tools/grit/grit/exception.py | 138 + tools/grit/grit/extern/BogoFP.py | 22 + tools/grit/grit/extern/FP.py | 71 + tools/grit/grit/extern/__init__.py | 0 tools/grit/grit/extern/tclib.py | 503 ++++ tools/grit/grit/format/__init__.py | 10 + tools/grit/grit/format/android_xml.py | 200 ++ tools/grit/grit/format/android_xml_unittest.py | 110 + tools/grit/grit/format/c_format.py | 86 + tools/grit/grit/format/c_format_unittest.py | 78 + tools/grit/grit/format/chrome_messages_json.py | 39 + .../grit/format/chrome_messages_json_unittest.py | 128 + tools/grit/grit/format/data_pack.py | 212 ++ tools/grit/grit/format/data_pack_unittest.py | 64 + tools/grit/grit/format/html_inline.py | 423 +++ tools/grit/grit/format/html_inline_unittest.py | 352 +++ tools/grit/grit/format/js_map_format.py | 44 + tools/grit/grit/format/js_map_format_unittest.py | 92 + .../grit/grit/format/policy_templates/PRESUBMIT.py | 29 + .../grit/grit/format/policy_templates/__init__.py | 10 + .../policy_templates/policy_template_generator.py | 154 + .../policy_template_generator_unittest.py | 395 +++ .../format/policy_templates/template_formatter.py | 73 + .../policy_templates/writer_configuration.py | 61 + .../format/policy_templates/writers/__init__.py | 10 + .../format/policy_templates/writers/adm_writer.py | 274 ++ .../writers/adm_writer_unittest.py | 1127 +++++++ .../format/policy_templates/writers/adml_writer.py | 186 ++ .../writers/adml_writer_unittest.py | 437 +++ .../format/policy_templates/writers/admx_writer.py | 382 +++ .../writers/admx_writer_unittest.py | 573 ++++ .../writers/android_policy_writer.py | 100 + .../writers/android_policy_writer_unittest.py | 82 + .../format/policy_templates/writers/doc_writer.py | 748 +++++ .../writers/doc_writer_unittest.py | 951 ++++++ .../policy_templates/writers/ios_plist_writer.py | 125 + .../writers/ios_plist_writer_unittest.py | 240 ++ .../format/policy_templates/writers/json_writer.py | 95 + .../writers/json_writer_unittest.py | 429 +++ .../format/policy_templates/writers/mock_writer.py | 30 + .../policy_templates/writers/plist_helper.py | 15 + .../writers/plist_strings_writer.py | 82 + .../writers/plist_strings_writer_unittest.py | 411 +++ .../policy_templates/writers/plist_writer.py | 149 + .../writers/plist_writer_unittest.py | 691 +++++ .../format/policy_templates/writers/reg_writer.py | 117 + .../writers/reg_writer_unittest.py | 392 +++ .../policy_templates/writers/template_writer.py | 318 ++ .../writers/template_writer_unittest.py | 84 + .../writers/writer_unittest_common.py | 83 + .../writers/xml_formatted_writer.py | 91 + .../writers/xml_writer_base_unittest.py | 40 + tools/grit/grit/format/rc.py | 481 +++ tools/grit/grit/format/rc_header.py | 204 ++ tools/grit/grit/format/rc_header_unittest.py | 193 ++ tools/grit/grit/format/rc_unittest.py | 409 +++ tools/grit/grit/format/repack.py | 37 + tools/grit/grit/format/resource_map.py | 132 + tools/grit/grit/format/resource_map_unittest.py | 279 ++ tools/grit/grit/gather/__init__.py | 9 + tools/grit/grit/gather/admin_template.py | 61 + tools/grit/grit/gather/admin_template_unittest.py | 117 + tools/grit/grit/gather/chrome_html.py | 360 +++ tools/grit/grit/gather/chrome_html_unittest.py | 523 ++++ tools/grit/grit/gather/chrome_scaled_image.py | 157 + .../grit/gather/chrome_scaled_image_unittest.py | 193 ++ tools/grit/grit/gather/igoogle_strings.py | 123 + tools/grit/grit/gather/igoogle_strings_unittest.py | 29 + tools/grit/grit/gather/interface.py | 171 ++ tools/grit/grit/gather/json_loader.py | 26 + tools/grit/grit/gather/muppet_strings.py | 133 + tools/grit/grit/gather/muppet_strings_unittest.py | 67 + tools/grit/grit/gather/policy_json.py | 251 ++ tools/grit/grit/gather/policy_json_unittest.py | 190 ++ tools/grit/grit/gather/rc.py | 343 +++ tools/grit/grit/gather/rc_unittest.py | 370 +++ tools/grit/grit/gather/regexp.py | 85 + tools/grit/grit/gather/skeleton_gatherer.py | 147 + tools/grit/grit/gather/tr_html.py | 745 +++++ tools/grit/grit/gather/tr_html_unittest.py | 522 ++++ tools/grit/grit/gather/txt.py | 37 + tools/grit/grit/gather/txt_unittest.py | 34 + tools/grit/grit/grd_reader.py | 221 ++ tools/grit/grit/grd_reader_unittest.py | 314 ++ tools/grit/grit/grit-todo.xml | 62 + tools/grit/grit/grit_runner.py | 272 ++ tools/grit/grit/grit_runner_unittest.py | 40 + tools/grit/grit/lazy_re.py | 45 + tools/grit/grit/lazy_re_unittest.py | 38 + tools/grit/grit/node/__init__.py | 9 + tools/grit/grit/node/base.py | 613 ++++ tools/grit/grit/node/base_unittest.py | 258 ++ tools/grit/grit/node/custom/__init__.py | 9 + tools/grit/grit/node/custom/filename.py | 28 + tools/grit/grit/node/custom/filename_unittest.py | 34 + tools/grit/grit/node/empty.py | 64 + tools/grit/grit/node/include.py | 152 + tools/grit/grit/node/include_unittest.py | 74 + tools/grit/grit/node/io.py | 119 + tools/grit/grit/node/io_unittest.py | 167 ++ tools/grit/grit/node/mapping.py | 61 + tools/grit/grit/node/message.py | 301 ++ tools/grit/grit/node/message_unittest.py | 90 + tools/grit/grit/node/misc.py | 552 ++++ tools/grit/grit/node/misc_unittest.py | 479 +++ tools/grit/grit/node/structure.py | 370 +++ tools/grit/grit/node/structure_unittest.py | 69 + tools/grit/grit/node/variant.py | 42 + tools/grit/grit/pseudo.py | 128 + tools/grit/grit/pseudo_rtl.py | 103 + tools/grit/grit/pseudo_unittest.py | 53 + tools/grit/grit/scons.py | 255 ++ tools/grit/grit/shortcuts.py | 93 + tools/grit/grit/shortcuts_unittests.py | 80 + tools/grit/grit/tclib.py | 235 ++ tools/grit/grit/tclib_unittest.py | 179 ++ tools/grit/grit/test_suite_all.py | 162 + 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 + tools/grit/grit/testdata/cache_prefix_file.html | 25 + tools/grit/grit/testdata/chat_result.html | 24 + .../testdata/chrome/app/generated_resources.grd | 199 ++ tools/grit/grit/testdata/chrome_html.html | 6 + tools/grit/grit/testdata/default_100_percent/a.png | 1 + tools/grit/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/details.html | 10 + tools/grit/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/grit/testdata/generated_resources_fr.xtb | 3090 ++++++++++++++++++++ .../grit/grit/testdata/generated_resources_iw.xtb | 4 + .../grit/grit/testdata/generated_resources_no.xtb | 4 + 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/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 tools/grit/grit/testdata/ko_oem_enable_bug.html | 1 + tools/grit/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/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 + tools/grit/grit/testdata/special_100_percent/a.png | 1 + tools/grit/grit/testdata/status.html | 44 + tools/grit/grit/testdata/structure_variables.html | 4 + tools/grit/grit/testdata/substitute.grd | 31 + tools/grit/grit/testdata/substitute.xmb | 10 + tools/grit/grit/testdata/substitute_no_ids.grd | 31 + tools/grit/grit/testdata/time_related.html | 11 + tools/grit/grit/testdata/toolbar_about.html | 138 + tools/grit/grit/testdata/tools/grit/resource_ids | 175 ++ tools/grit/grit/testdata/transl.rc | 56 + tools/grit/grit/testdata/versions.html | 7 + tools/grit/grit/testdata/whitelist.txt | 4 + tools/grit/grit/testdata/whitelist_resources.grd | 54 + tools/grit/grit/testdata/whitelist_strings.grd | 23 + tools/grit/grit/tool/__init__.py | 10 + tools/grit/grit/tool/android2grd.py | 479 +++ tools/grit/grit/tool/android2grd_unittest.py | 180 ++ tools/grit/grit/tool/build.py | 499 ++++ tools/grit/grit/tool/build_unittest.py | 353 +++ tools/grit/grit/tool/buildinfo.py | 68 + tools/grit/grit/tool/buildinfo_unittest.py | 87 + tools/grit/grit/tool/count.py | 35 + tools/grit/grit/tool/diff_structures.py | 114 + tools/grit/grit/tool/interface.py | 58 + tools/grit/grit/tool/menu_from_parts.py | 79 + tools/grit/grit/tool/newgrd.py | 70 + tools/grit/grit/tool/postprocess_interface.py | 32 + tools/grit/grit/tool/postprocess_unittest.py | 63 + tools/grit/grit/tool/preprocess_interface.py | 28 + tools/grit/grit/tool/preprocess_unittest.py | 49 + tools/grit/grit/tool/rc2grd.py | 409 +++ tools/grit/grit/tool/rc2grd_unittest.py | 137 + tools/grit/grit/tool/resize.py | 289 ++ tools/grit/grit/tool/test.py | 24 + tools/grit/grit/tool/toolbar_postprocess.py | 126 + tools/grit/grit/tool/toolbar_preprocess.py | 61 + tools/grit/grit/tool/transl2tc.py | 252 ++ tools/grit/grit/tool/transl2tc_unittest.py | 131 + tools/grit/grit/tool/unit.py | 26 + tools/grit/grit/tool/xmb.py | 291 ++ tools/grit/grit/tool/xmb_unittest.py | 103 + tools/grit/grit/util.py | 661 +++++ tools/grit/grit/util_unittest.py | 119 + tools/grit/grit/xtb_reader.py | 141 + tools/grit/grit/xtb_reader_unittest.py | 106 + tools/grit/grit_info.py | 193 ++ tools/licenses.py | 7 - 235 files changed, 36893 insertions(+), 11 deletions(-) create mode 100644 tools/grit/.gitignore create mode 100644 tools/grit/LICENSE create mode 100644 tools/grit/PRESUBMIT.py create mode 100644 tools/grit/README create mode 100644 tools/grit/codereview.settings create mode 100755 tools/grit/grit.py create mode 100755 tools/grit/grit/__init__.py create mode 100755 tools/grit/grit/clique.py create mode 100755 tools/grit/grit/clique_unittest.py create mode 100755 tools/grit/grit/constants.py create mode 100755 tools/grit/grit/exception.py create mode 100755 tools/grit/grit/extern/BogoFP.py create mode 100755 tools/grit/grit/extern/FP.py create mode 100644 tools/grit/grit/extern/__init__.py create mode 100755 tools/grit/grit/extern/tclib.py create mode 100755 tools/grit/grit/format/__init__.py create mode 100755 tools/grit/grit/format/android_xml.py create mode 100755 tools/grit/grit/format/android_xml_unittest.py create mode 100755 tools/grit/grit/format/c_format.py create mode 100755 tools/grit/grit/format/c_format_unittest.py create mode 100755 tools/grit/grit/format/chrome_messages_json.py create mode 100755 tools/grit/grit/format/chrome_messages_json_unittest.py create mode 100755 tools/grit/grit/format/data_pack.py create mode 100755 tools/grit/grit/format/data_pack_unittest.py create mode 100755 tools/grit/grit/format/html_inline.py create mode 100755 tools/grit/grit/format/html_inline_unittest.py create mode 100755 tools/grit/grit/format/js_map_format.py create mode 100755 tools/grit/grit/format/js_map_format_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/PRESUBMIT.py create mode 100755 tools/grit/grit/format/policy_templates/__init__.py create mode 100755 tools/grit/grit/format/policy_templates/policy_template_generator.py create mode 100755 tools/grit/grit/format/policy_templates/policy_template_generator_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/template_formatter.py create mode 100755 tools/grit/grit/format/policy_templates/writer_configuration.py create mode 100755 tools/grit/grit/format/policy_templates/writers/__init__.py create mode 100755 tools/grit/grit/format/policy_templates/writers/adm_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/adm_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/adml_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/adml_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/admx_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/admx_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/android_policy_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/android_policy_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/doc_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/ios_plist_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/ios_plist_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/json_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/json_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/mock_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/plist_helper.py create mode 100755 tools/grit/grit/format/policy_templates/writers/plist_strings_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/plist_strings_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/plist_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/plist_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/reg_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/reg_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/template_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/template_writer_unittest.py create mode 100755 tools/grit/grit/format/policy_templates/writers/writer_unittest_common.py create mode 100755 tools/grit/grit/format/policy_templates/writers/xml_formatted_writer.py create mode 100755 tools/grit/grit/format/policy_templates/writers/xml_writer_base_unittest.py create mode 100755 tools/grit/grit/format/rc.py create mode 100755 tools/grit/grit/format/rc_header.py create mode 100755 tools/grit/grit/format/rc_header_unittest.py create mode 100755 tools/grit/grit/format/rc_unittest.py create mode 100755 tools/grit/grit/format/repack.py create mode 100755 tools/grit/grit/format/resource_map.py create mode 100755 tools/grit/grit/format/resource_map_unittest.py create mode 100755 tools/grit/grit/gather/__init__.py create mode 100755 tools/grit/grit/gather/admin_template.py create mode 100755 tools/grit/grit/gather/admin_template_unittest.py create mode 100755 tools/grit/grit/gather/chrome_html.py create mode 100755 tools/grit/grit/gather/chrome_html_unittest.py create mode 100755 tools/grit/grit/gather/chrome_scaled_image.py create mode 100755 tools/grit/grit/gather/chrome_scaled_image_unittest.py create mode 100755 tools/grit/grit/gather/igoogle_strings.py create mode 100755 tools/grit/grit/gather/igoogle_strings_unittest.py create mode 100755 tools/grit/grit/gather/interface.py create mode 100755 tools/grit/grit/gather/json_loader.py create mode 100755 tools/grit/grit/gather/muppet_strings.py create mode 100755 tools/grit/grit/gather/muppet_strings_unittest.py create mode 100755 tools/grit/grit/gather/policy_json.py create mode 100755 tools/grit/grit/gather/policy_json_unittest.py create mode 100755 tools/grit/grit/gather/rc.py create mode 100755 tools/grit/grit/gather/rc_unittest.py create mode 100755 tools/grit/grit/gather/regexp.py create mode 100755 tools/grit/grit/gather/skeleton_gatherer.py create mode 100755 tools/grit/grit/gather/tr_html.py create mode 100755 tools/grit/grit/gather/tr_html_unittest.py create mode 100755 tools/grit/grit/gather/txt.py create mode 100755 tools/grit/grit/gather/txt_unittest.py create mode 100755 tools/grit/grit/grd_reader.py create mode 100755 tools/grit/grit/grd_reader_unittest.py create mode 100644 tools/grit/grit/grit-todo.xml create mode 100755 tools/grit/grit/grit_runner.py create mode 100755 tools/grit/grit/grit_runner_unittest.py create mode 100755 tools/grit/grit/lazy_re.py create mode 100755 tools/grit/grit/lazy_re_unittest.py create mode 100755 tools/grit/grit/node/__init__.py create mode 100755 tools/grit/grit/node/base.py create mode 100755 tools/grit/grit/node/base_unittest.py create mode 100755 tools/grit/grit/node/custom/__init__.py create mode 100755 tools/grit/grit/node/custom/filename.py create mode 100755 tools/grit/grit/node/custom/filename_unittest.py create mode 100755 tools/grit/grit/node/empty.py create mode 100755 tools/grit/grit/node/include.py create mode 100755 tools/grit/grit/node/include_unittest.py create mode 100755 tools/grit/grit/node/io.py create mode 100755 tools/grit/grit/node/io_unittest.py create mode 100755 tools/grit/grit/node/mapping.py create mode 100755 tools/grit/grit/node/message.py create mode 100755 tools/grit/grit/node/message_unittest.py create mode 100755 tools/grit/grit/node/misc.py create mode 100755 tools/grit/grit/node/misc_unittest.py create mode 100755 tools/grit/grit/node/structure.py create mode 100755 tools/grit/grit/node/structure_unittest.py create mode 100755 tools/grit/grit/node/variant.py create mode 100755 tools/grit/grit/pseudo.py create mode 100755 tools/grit/grit/pseudo_rtl.py create mode 100755 tools/grit/grit/pseudo_unittest.py create mode 100755 tools/grit/grit/scons.py create mode 100755 tools/grit/grit/shortcuts.py create mode 100755 tools/grit/grit/shortcuts_unittests.py create mode 100755 tools/grit/grit/tclib.py create mode 100755 tools/grit/grit/tclib_unittest.py create mode 100755 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/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/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/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/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 100755 tools/grit/grit/tool/__init__.py create mode 100755 tools/grit/grit/tool/android2grd.py create mode 100755 tools/grit/grit/tool/android2grd_unittest.py create mode 100755 tools/grit/grit/tool/build.py create mode 100755 tools/grit/grit/tool/build_unittest.py create mode 100755 tools/grit/grit/tool/buildinfo.py create mode 100755 tools/grit/grit/tool/buildinfo_unittest.py create mode 100755 tools/grit/grit/tool/count.py create mode 100755 tools/grit/grit/tool/diff_structures.py create mode 100755 tools/grit/grit/tool/interface.py create mode 100755 tools/grit/grit/tool/menu_from_parts.py create mode 100755 tools/grit/grit/tool/newgrd.py create mode 100755 tools/grit/grit/tool/postprocess_interface.py create mode 100755 tools/grit/grit/tool/postprocess_unittest.py create mode 100755 tools/grit/grit/tool/preprocess_interface.py create mode 100755 tools/grit/grit/tool/preprocess_unittest.py create mode 100755 tools/grit/grit/tool/rc2grd.py create mode 100755 tools/grit/grit/tool/rc2grd_unittest.py create mode 100755 tools/grit/grit/tool/resize.py create mode 100755 tools/grit/grit/tool/test.py create mode 100755 tools/grit/grit/tool/toolbar_postprocess.py create mode 100755 tools/grit/grit/tool/toolbar_preprocess.py create mode 100755 tools/grit/grit/tool/transl2tc.py create mode 100755 tools/grit/grit/tool/transl2tc_unittest.py create mode 100755 tools/grit/grit/tool/unit.py create mode 100755 tools/grit/grit/tool/xmb.py create mode 100755 tools/grit/grit/tool/xmb_unittest.py create mode 100755 tools/grit/grit/util.py create mode 100755 tools/grit/grit/util_unittest.py create mode 100755 tools/grit/grit/xtb_reader.py create mode 100755 tools/grit/grit/xtb_reader_unittest.py create mode 100755 tools/grit/grit_info.py diff --git a/.gitignore b/.gitignore index dc991b4..3c43406 100644 --- a/.gitignore +++ b/.gitignore @@ -430,7 +430,6 @@ v8.log /tools/gn/bin/linux /tools/gn/bin/mac /tools/gn/bin/win -/tools/grit /tools/gyp /tools/histograms /tools/json_schema_compiler/test/json_schema_compiler_tests.xml diff --git a/DEPS b/DEPS index ffce7ef..9986996 100644 --- a/DEPS +++ b/DEPS @@ -147,9 +147,6 @@ deps = { 'src/third_party/snappy/src': Var('chromium_git') + '/external/snappy.git' + '@' + '762bb32f0c9d2f31ba4958c7c0933d22e80c20bf', - 'src/tools/grit': - Var('chromium_git') + '/external/grit-i18n.git' + '@' + '4066463928d64337e6d70046aa4f18b833a2d0e3', # from svn revision 201 - 'src/tools/gyp': Var('chromium_git') + '/external/gyp.git' + '@' + '2c1e6cced23554ce84806e570acea637f6473afc', diff --git a/tools/copyright_scanner/third_party_files_whitelist.txt b/tools/copyright_scanner/third_party_files_whitelist.txt index a8eee6a..d451228 100644 --- a/tools/copyright_scanner/third_party_files_whitelist.txt +++ b/tools/copyright_scanner/third_party_files_whitelist.txt @@ -208,3 +208,6 @@ components/test_runner/helper/layout_test_helper_mac.mm # Bundles of existing code. chrome/browser/resources/md_downloads/crisper.js chrome/browser/resources/md_downloads/vulcanized.html +# Contains "copyright" in a test string. +# TODO(thakis): Change the test string and remove this entry again. +tools/grit/grit/tclib_unittest.py diff --git a/tools/grit/.gitignore b/tools/grit/.gitignore new file mode 100644 index 0000000..0d20b64 --- /dev/null +++ b/tools/grit/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/tools/grit/LICENSE b/tools/grit/LICENSE new file mode 100644 index 0000000..2aa3944 --- /dev/null +++ b/tools/grit/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2012 The Chromium Authors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tools/grit/PRESUBMIT.py b/tools/grit/PRESUBMIT.py new file mode 100644 index 0000000..7001689 --- /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.RunPythonUnitTests(input_api, output_api, + ['grit.test_suite_all']) + + +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 b/tools/grit/README new file mode 100644 index 0000000..8fcdafe --- /dev/null +++ b/tools/grit/README @@ -0,0 +1,2 @@ +GRIT (Google Resource and Internationalization Tool) is a tool for Windows +projects to manage resources and simplify the localization workflow. diff --git a/tools/grit/codereview.settings b/tools/grit/codereview.settings new file mode 100644 index 0000000..4535052 --- /dev/null +++ b/tools/grit/codereview.settings @@ -0,0 +1,6 @@ +# This file is used by gcl to get repository specific information. +CODE_REVIEW_SERVER: codereview.chromium.org +CC_LIST: grit-developer@googlegroups.com +VIEW_VC: http://code.google.com/p/grit-i18n/source/detail?r= +TRY_ON_UPLOAD: False + diff --git a/tools/grit/grit.py b/tools/grit/grit.py new file mode 100755 index 0000000..b17ceb9 --- /dev/null +++ b/tools/grit/grit.py @@ -0,0 +1,16 @@ +#!/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. +''' + +import sys + +import grit.grit_runner + + +if __name__ == '__main__': + grit.grit_runner.Main(sys.argv[1:]) + diff --git a/tools/grit/grit/__init__.py b/tools/grit/grit/__init__.py new file mode 100755 index 0000000..57e6709 --- /dev/null +++ b/tools/grit/grit/__init__.py @@ -0,0 +1,10 @@ +#!/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. + +'''Package 'grit' +''' + +pass + diff --git a/tools/grit/grit/clique.py b/tools/grit/grit/clique.py new file mode 100755 index 0000000..3a979890 --- /dev/null +++ b/tools/grit/grit/clique.py @@ -0,0 +1,483 @@ +#!/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. + +'''Collections of messages and their translations, called cliques. Also +collections of cliques (uber-cliques). +''' + +import re +import types + +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('(?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], 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], 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 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 , 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], types.StringTypes)): + 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 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(u'^\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.keys(): + 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('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.py new file mode 100755 index 0000000..faf54833 --- /dev/null +++ b/tools/grit/grit/clique_unittest.py @@ -0,0 +1,261 @@ +#!/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''' + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '..')) + +import re +import StringIO +import unittest + +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] != 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.StringIO(u''' + + + + + Hello %sJoi, how are you doing today? + + + + + + + +'''), 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.StringIO(u''' + + + + + + +'''), 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 100755 index 0000000..77faf2a --- /dev/null +++ b/tools/grit/grit/constants.py @@ -0,0 +1,18 @@ +#!/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. + +'''Constant definitions for GRIT. +''' + + +# 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' diff --git a/tools/grit/grit/exception.py b/tools/grit/grit/exception.py new file mode 100755 index 0000000..a9584a0 --- /dev/null +++ b/tools/grit/grit/exception.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. + +'''Exception types for GRIT. +''' + +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 element is allowed for each element.''' + pass + + +class GotPathExpectedFilenameOnly(Parsing): + '''The 'filename' attribute of and the 'file' attribute of + must be bare filenames, not paths. + ''' + 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 TooManyPlaceholders(Base): + '''Too many placeholders for elements of the same type.''' + 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 + diff --git a/tools/grit/grit/extern/BogoFP.py b/tools/grit/grit/extern/BogoFP.py new file mode 100755 index 0000000..3d9cad3 --- /dev/null +++ b/tools/grit/grit/extern/BogoFP.py @@ -0,0 +1,22 @@ +#!/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. + +"""Bogus fingerprint implementation, do not use for production, +provided only as an example. + +Usage: + grit.py -h grit.extern.BogoFP xmb /tmp/foo +""" + + +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 100755 index 0000000..3bde18d --- /dev/null +++ b/tools/grit/grit/extern/FP.py @@ -0,0 +1,71 @@ +#!/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. + +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).hexdigest() + int64 = long(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 & 0x8000000000000000L: + fp = - ((~fp & 0xFFFFFFFFFFFFFFFFL) + 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 0000000..e69de29 diff --git a/tools/grit/grit/extern/tclib.py b/tools/grit/grit/extern/tclib.py new file mode 100755 index 0000000..e84f177 --- /dev/null +++ b/tools/grit/grit/extern/tclib.py @@ -0,0 +1,503 @@ +#!/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. + +# 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. + +import exceptions + +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 & 0x7fffffffffffffffL) + +# ------------------------------------------------------------------------- +# The MessageTranslationError class is used to signal tclib-specific errors. + +class MessageTranslationError(exceptions.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 100755 index 0000000..2a3c59c --- /dev/null +++ b/tools/grit/grit/format/__init__.py @@ -0,0 +1,10 @@ +#!/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. + +'''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 100755 index 0000000..be9d795 --- /dev/null +++ b/tools/grit/grit/format/android_xml.py @@ -0,0 +1,200 @@ +#!/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. + +"""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 + + ... + + ... + + +for a grd file with the following messages: + + Hello + world + +and there existed an appropriate xtb file containing the Spanish translations, +then the output would be: + + + + "Hola" + "mundo" + + +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: + + Hello + +To generate Android plurals (aka "quantity strings"), use the ICU plural syntax +in the grd file. This will automatically be transformed into a element +in the output xml file. For example: + + + {NUM_CATS, plural, + =1 {1 cat} + other {# cats}} + + + will produce + + + 1 Katze + %d Katzen + +""" + +import os +import re +import types +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('IDS_(?P[A-Z0-9_]+)\Z') + +# Most strings are output as a element. Note the double quotes +# around the value to preserve whitespace. +_STRING_TEMPLATE = u'"%s"\n' + +# Some strings are output as a element. +_PLURALS_TEMPLATE = '\n%s\n' +_PLURALS_ITEM_TEMPLATE = ' %s\n' +_PLURALS_PATTERN = lazy_re.compile(r'\{[A-Z_]+,\s*plural,(?P.*)\}$', flags=re.S) +_PLURALS_ITEM_PATTERN = lazy_re.compile(r'(?P\S+)\s*\{(?P.*?)\}') +_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 ('\n' + '\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 '\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 element. + + 1. In a .grd file, we can write a plural string like this: + + + {NUM_THINGS, plural, + =1 {1 thing} + other {# things}} + + + 2. The Android equivalent looks like this: + + + 1 thing + %d things + + + 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 = [] + 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: + 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 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 100755 index 0000000..cdec6c9 --- /dev/null +++ b/tools/grit/grit/format/android_xml_unittest.py @@ -0,0 +1,110 @@ +#!/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.""" + +import os +import StringIO +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 android_xml +from grit.node import message +from grit.tool import build + + +class AndroidXmlUnittest(unittest.TestCase): + + def testMessages(self): + root = util.ParseGrdForUnittest(ur""" + + + Martha + + sat and wondered + + out loud, "Why don't I build a flying car?" + + + She gathered +wood, charcoal, and +a sledge hammer. + + + ''' How old fashioned -- she thought. ''' + + + I'll buy a %d200 nm laser at %sthe grocery store. + + + {NUM_THINGS, plural, + =1 {Maybe I'll get one laser.} + other {Maybe I'll get # lasers.}} + + + """) + + buf = StringIO.StringIO() + build.RcBuilder.ProcessNode(root, DummyOutput('android', 'en'), buf) + output = buf.getvalue() + expected = ur""" + + +"Martha" +"sat and wondered" +"out loud, \"Why don\'t I build a flying car?\"" +"She gathered +wood, charcoal, and +a sledge hammer." +" How old fashioned -- she thought. " +"I\'ll buy a %d nm laser at %s." + + "Maybe I\'ll get one laser." + "Maybe I\'ll get %d lasers." + + +""" + self.assertEqual(output.strip(), expected.strip()) + + def testTaggedOnly(self): + root = util.ParseGrdForUnittest(ur""" + + + Hello + + + world + + + """) + + 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 100755 index 0000000..5e10b98 --- /dev/null +++ b/tools/grit/grit/format/c_format.py @@ -0,0 +1,86 @@ +#!/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. + +"""Formats as a .C file for compilation. +""" + +import os +import re +import types + +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, types.StringTypes) + + 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 += "\\" + '%03d' % int(oct(next_num), 10) + hex = hex[4:] + return match.group("escaped_backslashes") + result + + +def _FormatMessage(item, lang): + """Format a single 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 + message = message.encode('utf-8').encode('string_escape') + # 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%s)(?P%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 100755 index 0000000..ba1c5c7 --- /dev/null +++ b/tools/grit/grit/format/c_format_unittest.py @@ -0,0 +1,78 @@ +#!/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. +""" + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import unittest +import StringIO + +from grit import util +from grit.tool import build + + +class CFormatUnittest(unittest.TestCase): + + def testMessages(self): + root = util.ParseGrdForUnittest(""" + + Do you want to play questions? + + "What's in a name, %sBrandon?" + + + Was that rhetoric? +No. +Statement. Two all. Game point. + + + \xc3\xb5\\xc2\\xa4\\\xc2\xa4\\\\xc3\\xb5\xe4\xa4\xa4 + + + """) + + buf = StringIO.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 100755 index 0000000..be934ab --- /dev/null +++ b/tools/grit/grit/format/chrome_messages_json.py @@ -0,0 +1,39 @@ +#!/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. + +"""Formats as a .json file that can be used to localize Google Chrome +extensions.""" + +from json import JSONEncoder +import re +import types + +from grit import util +from grit.node import message + +def Format(root, lang='en', output_dir='.'): + """Format the messages as JSON.""" + yield '{\n' + + encoder = JSONEncoder(); + format = (' "%s": {\n' + ' "message": %s\n' + ' }') + 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:] + + loc_message = encoder.encode(child.ws_at_start + child.Translate(lang) + + child.ws_at_end) + + if not first: + yield ',\n' + first = False + yield format % (id, loc_message) + + yield '\n}\n' 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 100755 index 0000000..373751ead --- /dev/null +++ b/tools/grit/grit/format/chrome_messages_json_unittest.py @@ -0,0 +1,128 @@ +#!/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. +""" + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import unittest +import StringIO + +from grit import grd_reader +from grit import util +from grit.tool import build + +class ChromeMessagesJsonFormatUnittest(unittest.TestCase): + + def testMessages(self): + root = util.ParseGrdForUnittest(u""" + + + Simple message. + + + element\u2019s \u201c%sname\u201d attribute + + + %1$d1 error, %2$d1 warning + + + ''' (%d2) + + + (%d2) ''' + + + ''' (%d2) ''' + + + A "double quoted" message. + + + \\ + + + """) + + buf = StringIO.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" + }, + "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(test.strip(), output.strip()) + + def testTranslations(self): + root = util.ParseGrdForUnittest(""" + + Hello! + Hello %s + Joi + + """) + + buf = StringIO.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(test.strip(), output.strip()) + + +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 100755 index 0000000..02616c39 --- /dev/null +++ b/tools/grit/grit/format/data_pack.py @@ -0,0 +1,212 @@ +#!/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. +""" + +import collections +import exceptions +import os +import struct +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +from grit import util +from grit.node import include +from grit.node import message +from grit.node import structure + + +PACK_FILE_VERSION = 4 +HEADER_LENGTH = 2 * 4 + 1 # Two uint32s. (file version, number of entries) and + # one uint8 (encoding of text resources) +BINARY, UTF8, UTF16 = range(3) + + +class WrongFileVersion(Exception): + pass + + +DataPackContents = collections.namedtuple( + 'DataPackContents', 'resources encoding') + + +def Format(root, lang='en', output_dir='.'): + """Writes out the data pack file format (platform agnostic resource file).""" + data = {} + for node in root.ActiveDescendants(): + with node: + if isinstance(node, (include.IncludeNode, message.MessageNode, + structure.StructureNode)): + id, value = node.GetDataPackPair(lang, UTF8) + if value is not None: + data[id] = value + return WriteDataPackToString(data, UTF8) + + +def ReadDataPack(input_file): + """Reads a data pack file and returns a dictionary.""" + data = util.ReadFile(input_file, util.BINARY) + original_data = data + + # Read the header. + version, num_entries, encoding = struct.unpack('data in the data pack format.""" + ids = sorted(resources.keys()) + ret = [] + + # Write file header. + ret.append(struct.pack('data into output_file as a data pack.""" + content = WriteDataPackToString(resources, encoding) + with open(output_file, 'wb') as file: + file.write(content) + + +def RePack(output_file, input_files, whitelist_file=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. + + Raises: + KeyError: if there are duplicate keys or resource encoding is + inconsistent. + """ + input_data_packs = [ReadDataPack(filename) for filename in input_files] + whitelist = None + if whitelist_file: + whitelist = util.ReadFile(whitelist_file, util.RAW_TEXT).strip().split('\n') + whitelist = set(map(int, whitelist)) + resources, encoding = RePackFromDataPackStrings(input_data_packs, whitelist) + WriteDataPack(resources, output_file, encoding) + + +def RePackFromDataPackStrings(inputs, whitelist): + """Returns a data pack string that combines the resources from inputs. + + Args: + inputs: a list of data pack strings that need to be combined. + whitelist: a list of resource IDs that should be kept in the output string + or None to include all resources. + + Returns: + DataPackContents: a tuple containing the new combined data pack and its + encoding. + + Raises: + KeyError: if there are duplicate keys or resource encoding is + inconsistent. + """ + resources = {} + encoding = None + for content in inputs: + # Make sure we have no dups. + duplicate_keys = set(content.resources.keys()) & set(resources.keys()) + if duplicate_keys: + raise exceptions.KeyError('Duplicate keys: ' + str(list(duplicate_keys))) + + # Make sure encoding is consistent. + if encoding in (None, BINARY): + encoding = content.encoding + elif content.encoding not in (BINARY, encoding): + raise exceptions.KeyError('Inconsistent encodings: ' + str(encoding) + + ' vs ' + str(content.encoding)) + + if whitelist: + whitelisted_resources = dict([(key, content.resources[key]) + for key in content.resources.keys() + if key in whitelist]) + resources.update(whitelisted_resources) + removed_keys = [key for key in content.resources.keys() + if key not in whitelist] + for key in removed_keys: + print 'RePackFromDataPackStrings Removed Key:', key + else: + resources.update(content.resources) + + # Encoding is 0 for BINARY, 1 for UTF8 and 2 for UTF16 + if encoding is None: + encoding = BINARY + return DataPackContents(resources, encoding) + + +# Temporary hack for external programs that import data_pack. +# TODO(benrg): Remove this. +class DataPack(object): + pass +DataPack.ReadDataPack = staticmethod(ReadDataPack) +DataPack.WriteDataPackToString = staticmethod(WriteDataPackToString) +DataPack.WriteDataPack = staticmethod(WriteDataPack) +DataPack.RePack = staticmethod(RePack) + + +def main(): + if len(sys.argv) > 1: + # When an argument is given, read and explode the file to text + # format, for easier diffing. + data = ReadDataPack(sys.argv[1]) + print data.encoding + for (resource_id, text) in data.resources.iteritems(): + print '%s: %s' % (resource_id, text) + else: + # Just 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 100755 index 0000000..f6e9edc --- /dev/null +++ b/tools/grit/grit/format/data_pack_unittest.py @@ -0,0 +1,64 @@ +#!/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''' + + +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 testWriteDataPack(self): + expected = ( + '\x04\x00\x00\x00' # header(version + '\x04\x00\x00\x00' # no. entries, + '\x01' # encoding) + '\x01\x00\x27\x00\x00\x00' # index entry 1 + '\x04\x00\x27\x00\x00\x00' # index entry 4 + '\x06\x00\x33\x00\x00\x00' # index entry 6 + '\x0a\x00\x3f\x00\x00\x00' # index entry 10 + '\x00\x00\x3f\x00\x00\x00' # extra entry for the size of last + 'this is id 4this is id 6') # data + input = {1: '', 4: 'this is id 4', 6: 'this is id 6', 10: ''} + output = data_pack.WriteDataPackToString(input, data_pack.UTF8) + self.failUnless(output == expected) + + 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 = [data_pack.DataPackContents(input, data_pack.UTF8) for input + in inputs] + + # RePack using whitelist + output, _ = data_pack.RePackFromDataPackStrings(inputs, whitelist) + self.assertDictEqual(expected_with_whitelist, output, + 'Incorrect resource output') + + # RePack a None whitelist + output, _ = data_pack.RePackFromDataPackStrings(inputs, None) + self.assertDictEqual(expected_without_whitelist, output, + 'Incorrect resource output') + + +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 100755 index 0000000..f532496 --- /dev/null +++ b/tools/grit/grit/format/html_inline.py @@ -0,0 +1,423 @@ +#!/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. +""" + +import os +import re +import sys +import base64 +import mimetypes + +from grit import lazy_re +from grit import util + +# 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') + +DIST_DEFAULT = 'chromium' +DIST_ENV_VAR = 'CHROMIUM_BUILD' +DIST_SUBSTR = '%DISTRIBUTION%' + +# Matches beginning of an "if" block with trailing spaces. +_BEGIN_IF_BLOCK = lazy_re.compile( + ']*?expr="(?P[^"]*)"[^>]*?>\s*') + +# Matches ending of an "if" block with preceding spaces. +_END_IF_BLOCK = lazy_re.compile('\s*') + +# Used by DoInline to replace various links with inline content. +_STYLESHEET_RE = lazy_re.compile( + ']+?href="(?P[^"]*)".*?>(\s*)?', + re.DOTALL) +_INCLUDE_RE = lazy_re.compile( + ']+?src="(?P[^"\']*)".*?>(\s*)?', + re.DOTALL) +_SRC_RE = lazy_re.compile( + r'<(?!script)(?:[^>]+?\s)src=(?P")(?!\[\[|{{)(?P[^"\']*)\1', + re.MULTILINE) +_ICON_RE = lazy_re.compile( + r']+?\s)?' + 'href=(?P")(?P[^"\']*)\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.keys(): + distribution = os.environ[DIST_ENV_VAR] + if len(distribution) > 1 and distribution[0] == '_': + distribution = distribution[1:].lower() + return distribution + + +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' and 'quote' named capturing + groups + 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) + quote = src_match.group('quote') + + if filename.find(':') != -1: + # filename is probably a URL, which we don't want to bother inlining + return src_match.group(0) + + 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)) + + prefix = src_match.string[src_match.start():src_match.start('filename')] + suffix = src_match.string[src_match.end('filename'):src_match.end()] + return '%sdata:%s;base64,%s%s' % (prefix, mimetype, inline_data, 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, names_only=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 + names_only: |nil| will be returned for the inlined contents (faster). + 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 GetFilepath(src_match, base_path = input_filepath): + filename = src_match.group('filename') + + 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): + expression = src_match.group('expression') + return grd_node is None or grd_node.EvaluateCondition(expression) + + 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 ') + 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 ') + + 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): + """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, + allow_external_script, + rewrite_function, + filename_expansion_function=filename_expansion_function)) + return "" + + return pattern % InlineToString( + filepath, grd_node, allow_external_script, + filename_expansion_function=filename_expansion_function) + + 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, '%s') + + 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) + # When resolving CSS files we need to pass in the path so that relative URLs + # can be resolved. + return pattern % InlineCSSText(util.ReadFile(filepath, util.BINARY), + filepath) + + 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. + return re.sub('(content|background|[\w-]*-image):[^;]*' + + '(url\((?P"|\'|)[^"\'()]*(?P=quote1)\)|' + + 'image-set\(' + + '([ ]*url\((?P"|\'|)[^"\'()]*(?P=quote2)\)' + + '[ ]*[0-9.]*x[ ]*(,[ ]*)?)+\))', + 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('url\((?P"|\'|)(?P[^"\'()]*)(?P=quote)\)', + 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('@import\s+url\((?P"|\'|)(?P[^"\'()]*)' + + '(?P=quote)\);', + lambda m: InlineCSSFile(m, '%s', filepath), + text) + + + flat_text = util.ReadFile(input_filename, util.BINARY) + + # 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) + + 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('', + InlineScript, + flat_text) + + flat_text = _STYLESHEET_RE.sub( + lambda m: InlineCSSFile(m, ''), + flat_text) + + flat_text = _INCLUDE_RE.sub(InlineIncludeFiles, 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) + + flat_text = _SRC_RE.sub(SrcReplace, flat_text) + + # TODO(arv): Only do this inside + + + ''' + + 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 testInlineCSSLinks(self): + '''Tests that only CSS files referenced via relative URLs are inlined.''' + + files = { + 'index.html': ''' + + + + + + + ''', + + 'foo.css': ''' + @import url(chrome://resources/blurp.css); + blink { + display: none; + } + ''', + } + + expected_inlined = ''' + + + + + + + ''' + + 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')) + + def testFilenameVariableExpansion(self): + '''Tests that variables are expanded in filenames before inlining.''' + + files = { + 'index.html': ''' + + + + + + + + + ''', + 'style1.css': '''h1 {}''', + 'tmpl1.html': '''

''', + 'script1.js': '''console.log('hello');''', + 'img1.png': '''abc''', + } + + expected_inlined = ''' + + + + + +

+ + + ''' + + 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) + + def testWithCloseTags(self): + '''Tests that close tags are removed.''' + + files = { + 'index.html': ''' + + + + + + + + + + + + + + + + + ''', + 'style1.css': '''h1 {}''', + 'style2.css': '''h2 {}''', + 'tmpl1.html': '''

''', + 'tmpl2.html': '''

''', + 'script1.js': '''console.log('hello');''', + 'img1.png': '''abc''', + } + + expected_inlined = ''' + + + + + + + +

+

+

+ + + ''' + + 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')) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/js_map_format.py b/tools/grit/grit/format/js_map_format.py new file mode 100755 index 0000000..8cc8eb2 --- /dev/null +++ b/tools/grit/grit/format/js_map_format.py @@ -0,0 +1,44 @@ +#!/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. + +"""Formats as a .js file using a map: -> . +""" + +import re + +from grit import util + + +"""The required preamble for JS files.""" +_HEADER = '// This file is automatically generated by GRIT. Do not edit.\n' + + +def Format(root, lang='en', output_dir='.'): + from grit.node import empty, message + yield _HEADER + for item in root.ActiveDescendants(): + with item: + if isinstance(item, message.MessageNode): + yield _FormatMessage(item, lang) + elif isinstance(item, empty.MessagesNode): + yield '\n' + + +def _FormatMessage(item, lang): + """Format a single message.""" + + en_message = item.ws_at_start + item.Translate('en') + item.ws_at_end + # Remove position numbers from placeholders. + en_message = re.sub(r'%\d\$([a-z])', r'%\1', en_message) + # Escape double quotes. + en_message = re.sub(r'\\', r'\\\\', en_message) + en_message = re.sub(r'"', r'\"', en_message) + + loc_message = item.ws_at_start + item.Translate(lang) + item.ws_at_end + # Escape double quotes. + loc_message = re.sub(r'\\', r'\\\\', loc_message) + loc_message = re.sub(r'"', r'\"', loc_message) + + return '\nlocalizedStrings["%s"] = "%s";' % (en_message, loc_message) diff --git a/tools/grit/grit/format/js_map_format_unittest.py b/tools/grit/grit/format/js_map_format_unittest.py new file mode 100755 index 0000000..cac0b2e --- /dev/null +++ b/tools/grit/grit/format/js_map_format_unittest.py @@ -0,0 +1,92 @@ +#!/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 js_map_format.py. +""" + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import unittest +import StringIO + +from grit import util +from grit.tool import build + + +class JsMapFormatUnittest(unittest.TestCase): + + def testMessages(self): + root = util.ParseGrdForUnittest(u""" + + + Simple message. + + + element\u2019s \u201c%sname\u201d attribute + + + %1$d1 error, %2$d1 warning + + + ''' (%d2) + + + A "double quoted" message. + + + \\ + + + """) + + buf = StringIO.StringIO() + build.RcBuilder.ProcessNode(root, DummyOutput('js_map_format', 'en'), buf) + output = util.StripBlankLinesAndComments(buf.getvalue()) + self.assertEqual(u"""\ +localizedStrings["Simple message."] = "Simple message."; +localizedStrings["element\u2019s \u201c%s\u201d attribute"] = "element\u2019s \u201c%s\u201d attribute"; +localizedStrings["%d error, %d warning"] = "%1$d error, %2$d warning"; +localizedStrings[" (%d)"] = " (%d)"; +localizedStrings["A \\\"double quoted\\\" message."] = "A \\\"double quoted\\\" message."; +localizedStrings["\\\\"] = "\\\\";""", output) + + def testTranslations(self): + root = util.ParseGrdForUnittest(""" + + Hello! + Hello %s + Joi + + """) + + buf = StringIO.StringIO() + build.RcBuilder.ProcessNode(root, DummyOutput('js_map_format', 'fr'), buf) + output = util.StripBlankLinesAndComments(buf.getvalue()) + self.assertEqual(u"""\ +localizedStrings["Hello!"] = "H\xe9P\xe9ll\xf4P\xf4!"; +localizedStrings["Hello %s"] = "H\xe9P\xe9ll\xf4P\xf4 %s";\ +""", 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/policy_templates/PRESUBMIT.py b/tools/grit/grit/format/policy_templates/PRESUBMIT.py new file mode 100755 index 0000000..dca24a0 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/PRESUBMIT.py @@ -0,0 +1,29 @@ +#!/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 = [ + 'policy_template_generator_unittest', + 'writers.adm_writer_unittest', + 'writers.adml_writer_unittest', + 'writers.admx_writer_unittest', + 'writers.android_policy_writer_unittest', + 'writers.doc_writer_unittest', + 'writers.json_writer_unittest', + 'writers.plist_strings_writer_unittest', + 'writers.plist_writer_unittest', + 'writers.reg_writer_unittest', + 'writers.template_writer_unittest' +] + +def CheckChangeOnUpload(input_api, output_api): + return input_api.canned_checks.RunPythonUnitTests(input_api, + output_api, + UNIT_TESTS) + + +def CheckChangeOnCommit(input_api, output_api): + return input_api.canned_checks.RunPythonUnitTests(input_api, + output_api, + UNIT_TESTS) diff --git a/tools/grit/grit/format/policy_templates/__init__.py b/tools/grit/grit/format/policy_templates/__init__.py new file mode 100755 index 0000000..21cab65 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/__init__.py @@ -0,0 +1,10 @@ +#!/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. + +'''Module grit.format.policy_templates +''' + +pass + diff --git a/tools/grit/grit/format/policy_templates/policy_template_generator.py b/tools/grit/grit/format/policy_templates/policy_template_generator.py new file mode 100755 index 0000000..a1eb123 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/policy_template_generator.py @@ -0,0 +1,154 @@ +#!/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. + + +import copy + + +class PolicyTemplateGenerator: + '''Generates template text for a particular platform. + + This class is used to traverse a JSON structure from a .json template + definition metafile and merge GUI message string definitions that come + from a .grd resource tree onto it. After this, it can be used to output + this data to policy template files using TemplateWriter objects. + ''' + + def _ImportMessage(self, msg_txt): + msg_txt = msg_txt.decode('utf-8') + # Replace the placeholder of app name. + msg_txt = msg_txt.replace('$1', self._config['app_name']) + msg_txt = msg_txt.replace('$2', self._config['os_name']) + msg_txt = msg_txt.replace('$3', self._config['frame_name']) + # Strip spaces and escape newlines. + lines = msg_txt.split('\n') + lines = [line.strip() for line in lines] + return "\n".join(lines) + + def __init__(self, config, policy_data): + '''Initializes this object with all the data necessary to output a + policy template. + + Args: + messages: An identifier to string dictionary of all the localized + messages that might appear in the policy template. + policy_definitions: The list of defined policies and groups, as + parsed from the policy metafile. Note that this list is passed by + reference and its contents are modified. + See chrome/app/policy.policy_templates.json for description and + content. + ''' + # List of all the policies: + self._policy_data = copy.deepcopy(policy_data) + # Localized messages to be inserted to the policy_definitions structure: + self._messages = self._policy_data['messages'] + self._config = config + for key in self._messages.keys(): + self._messages[key]['text'] = self._ImportMessage( + self._messages[key]['text']) + self._policy_definitions = self._policy_data['policy_definitions'] + self._ProcessPolicyList(self._policy_definitions) + + def _ProcessSupportedOn(self, supported_on): + '''Parses and converts the string items of the list of supported platforms + into dictionaries. + + Args: + supported_on: The list of supported platforms. E.g.: + ['chrome.win:8-10', 'chrome_frame:10-'] + + Returns: + supported_on: The list with its items converted to dictionaries. E.g.: + [{ + 'product': 'chrome', + 'platform': 'win', + 'since_version': '8', + 'until_version': '10' + }, { + 'product': 'chrome_frame', + 'platform': 'win', + 'since_version': '10', + 'until_version': '' + }] + ''' + result = [] + for supported_on_item in supported_on: + product_platform_part, version_part = supported_on_item.split(':') + + if '.' in product_platform_part: + product, platform = product_platform_part.split('.') + if platform == '*': + # e.g.: 'chrome.*:8-10' + platforms = ['linux', 'mac', 'win'] + else: + # e.g.: 'chrome.win:-10' + platforms = [platform] + else: + # e.g.: 'chrome_frame:7-' + product, platform = { + 'android': ('chrome', 'android'), + 'chrome_os': ('chrome_os', 'chrome_os'), + 'chrome_frame': ('chrome_frame', 'win'), + 'ios': ('chrome', 'ios'), + }[product_platform_part] + platforms = [platform] + since_version, until_version = version_part.split('-') + result.append({ + 'product': product, + 'platforms': platforms, + 'since_version': since_version, + 'until_version': until_version + }) + return result + + def _ProcessPolicy(self, policy): + '''Processes localized message strings in a policy or a group. + Also breaks up the content of 'supported_on' attribute into a list. + + Args: + policy: The data structure of the policy or group, that will get message + strings here. + ''' + policy['desc'] = self._ImportMessage(policy['desc']) + policy['caption'] = self._ImportMessage(policy['caption']) + if 'label' in policy: + policy['label'] = self._ImportMessage(policy['label']) + + if policy['type'] == 'group': + self._ProcessPolicyList(policy['policies']) + elif policy['type'] in ('string-enum', 'int-enum', 'string-enum-list'): + # Iterate through all the items of an enum-type policy, and add captions. + for item in policy['items']: + item['caption'] = self._ImportMessage(item['caption']) + if policy['type'] != 'group': + if not 'label' in policy: + # If 'label' is not specified, then it defaults to 'caption': + policy['label'] = policy['caption'] + policy['supported_on'] = self._ProcessSupportedOn(policy['supported_on']) + + def _ProcessPolicyList(self, policy_list): + '''Adds localized message strings to each item in a list of policies and + groups. Also breaks up the content of 'supported_on' attributes into lists + of dictionaries. + + Args: + policy_list: A list of policies and groups. Message strings will be added + for each item and to their child items, recursively. + ''' + for policy in policy_list: + self._ProcessPolicy(policy) + + def GetTemplateText(self, template_writer): + '''Generates the text of the template from the arguments given + to the constructor, using a given TemplateWriter. + + Args: + template_writer: An object implementing TemplateWriter. Its methods + are called here for each item of self._policy_groups. + + Returns: + The text of the generated template. + ''' + return template_writer.WriteTemplate(self._policy_data) diff --git a/tools/grit/grit/format/policy_templates/policy_template_generator_unittest.py b/tools/grit/grit/format/policy_templates/policy_template_generator_unittest.py new file mode 100755 index 0000000..adc4a22 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/policy_template_generator_unittest.py @@ -0,0 +1,395 @@ +#!/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. + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../..')) + +import unittest + +from grit.format.policy_templates import policy_template_generator +from grit.format.policy_templates.writers import mock_writer +from grit.format.policy_templates.writers import template_writer + + +class PolicyTemplateGeneratorUnittest(unittest.TestCase): + '''Unit tests for policy_template_generator.py.''' + + def do_test(self, policy_data, writer): + '''Executes a test case. + + Creates and invokes an instance of PolicyTemplateGenerator with + the given arguments. + + Notice: Plain comments are used in test methods instead of docstrings, + so that method names do not get overridden by the docstrings in the + test output. + + Args: + policy_data: The list of policies and groups as it would be + loaded from policy_templates.json. + writer: A writer used for this test. It is usually derived from + mock_writer.MockWriter. + ''' + writer.tester = self + config = { + 'app_name': '_app_name', + 'frame_name': '_frame_name', + 'os_name': '_os_name', + } + if not 'messages' in policy_data: + policy_data['messages'] = {} + if not 'placeholders' in policy_data: + policy_data['placeholders'] = [] + if not 'policy_definitions' in policy_data: + policy_data['policy_definitions'] = [] + policy_generator = policy_template_generator.PolicyTemplateGenerator( + config, + policy_data) + res = policy_generator.GetTemplateText(writer) + writer.Test() + return res + + def testSequence(self): + # Test the sequence of invoking the basic PolicyWriter operations, + # in case of empty input data structures. + class LocalMockWriter(mock_writer.MockWriter): + def __init__(self): + self.log = 'init;' + def Init(self): + self.log += 'prepare;' + def BeginTemplate(self): + self.log += 'begin;' + def EndTemplate(self): + self.log += 'end;' + def GetTemplateText(self): + self.log += 'get_text;' + return 'writer_result_string' + def Test(self): + self.tester.assertEquals(self.log, + 'init;prepare;begin;end;get_text;') + result = self.do_test({}, LocalMockWriter()) + self.assertEquals(result, 'writer_result_string') + + def testEmptyGroups(self): + # Test that empty policy groups are not passed to the writer. + policies_mock = { + 'policy_definitions': [ + {'name': 'Group1', 'type': 'group', 'policies': [], + 'desc': '', 'caption': ''}, + {'name': 'Group2', 'type': 'group', 'policies': [], + 'desc': '', 'caption': ''}, + {'name': 'Group3', 'type': 'group', 'policies': [], + 'desc': '', 'caption': ''}, + ] + } + class LocalMockWriter(mock_writer.MockWriter): + def __init__(self): + self.log = '' + def BeginPolicyGroup(self, group): + self.log += '[' + def EndPolicyGroup(self): + self.log += ']' + def Test(self): + self.tester.assertEquals(self.log, '') + self.do_test(policies_mock, LocalMockWriter()) + + def testGroups(self): + # Test that policy groups are passed to the writer in the correct order. + policies_mock = { + 'policy_definitions': [ + { + 'name': 'Group1', 'type': 'group', + 'caption': '', 'desc': '', + 'policies': [{'name': 'TAG1', 'type': 'mock', 'supported_on': [], + 'caption': '', 'desc': ''}] + }, + { + 'name': 'Group2', 'type': 'group', + 'caption': '', 'desc': '', + 'policies': [{'name': 'TAG2', 'type': 'mock', 'supported_on': [], + 'caption': '', 'desc': ''}] + }, + { + 'name': 'Group3', 'type': 'group', + 'caption': '', 'desc': '', + 'policies': [{'name': 'TAG3', 'type': 'mock', 'supported_on': [], + 'caption': '', 'desc': ''}] + }, + ] + } + class LocalMockWriter(mock_writer.MockWriter): + def __init__(self): + self.log = '' + def BeginPolicyGroup(self, group): + self.log += '[' + group['policies'][0]['name'] + def EndPolicyGroup(self): + self.log += ']' + def Test(self): + self.tester.assertEquals(self.log, '[TAG1][TAG2][TAG3]') + self.do_test(policies_mock, LocalMockWriter()) + + def testPolicies(self): + # Test that policies are passed to the writer in the correct order. + policy_defs_mock = { + 'policy_definitions': [ + { + 'name': 'Group1', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [ + {'name': 'Group1Policy1', 'type': 'string', 'supported_on': [], + 'caption': '', 'desc': ''}, + {'name': 'Group1Policy2', 'type': 'string', 'supported_on': [], + 'caption': '', 'desc': ''}, + ] + }, + { + 'name': 'Group2', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [ + {'name': 'Group2Policy3', 'type': 'string', 'supported_on': [], + 'caption': '', 'desc': ''}, + ] + } + ] + } + class LocalMockWriter(mock_writer.MockWriter): + def __init__(self): + self.policy_name = None + self.policy_list = [] + def BeginPolicyGroup(self, group): + self.group = group; + def EndPolicyGroup(self): + self.group = None + def WritePolicy(self, policy): + self.tester.assertEquals(policy['name'][0:6], self.group['name']) + self.policy_list.append(policy['name']) + def Test(self): + self.tester.assertEquals( + self.policy_list, + ['Group1Policy1', 'Group1Policy2', 'Group2Policy3']) + self.do_test( policy_defs_mock, LocalMockWriter()) + + def testPolicyTexts(self): + # Test that GUI messages of policies all get placeholders replaced. + policy_data_mock = { + 'policy_definitions': [ + { + 'name': 'Group1', + 'type': 'group', + 'desc': '', + 'caption': '', + 'policies': [ + { + 'name': 'Policy1', + 'caption': '1. app_name -- $1', + 'label': '2. os_name -- $2', + 'desc': '3. frame_name -- $3', + 'type': 'string', + 'supported_on': [] + }, + ] + } + ] + } + class LocalMockWriter(mock_writer.MockWriter): + def WritePolicy(self, policy): + if policy['name'] == 'Policy1': + self.tester.assertEquals(policy['caption'], + '1. app_name -- _app_name') + self.tester.assertEquals(policy['label'], + '2. os_name -- _os_name') + self.tester.assertEquals(policy['desc'], + '3. frame_name -- _frame_name') + elif policy['name'] == 'Group1': + pass + else: + self.tester.fail() + self.do_test(policy_data_mock, LocalMockWriter()) + + def testIntEnumTexts(self): + # Test that GUI messages are assigned correctly to int-enums + # (aka dropdown menus). + policy_defs_mock = { + 'policy_definitions': [{ + 'name': 'Policy1', + 'type': 'int-enum', + 'caption': '', 'desc': '', + 'supported_on': [], + 'items': [ + {'name': 'item1', 'value': 0, 'caption': 'string1', 'desc': ''}, + {'name': 'item2', 'value': 1, 'caption': 'string2', 'desc': ''}, + {'name': 'item3', 'value': 3, 'caption': 'string3', 'desc': ''}, + ] + }] + } + + class LocalMockWriter(mock_writer.MockWriter): + def WritePolicy(self, policy): + self.tester.assertEquals(policy['items'][0]['caption'], 'string1') + self.tester.assertEquals(policy['items'][1]['caption'], 'string2') + self.tester.assertEquals(policy['items'][2]['caption'], 'string3') + self.do_test(policy_defs_mock, LocalMockWriter()) + + def testStringEnumTexts(self): + # Test that GUI messages are assigned correctly to string-enums + # (aka dropdown menus). + policy_data_mock = { + 'policy_definitions': [{ + 'name': 'Policy1', + 'type': 'string-enum', + 'caption': '', 'desc': '', + 'supported_on': [], + 'items': [ + {'name': 'item1', 'value': 'one', 'caption': 'string1', 'desc': ''}, + {'name': 'item2', 'value': 'two', 'caption': 'string2', 'desc': ''}, + {'name': 'item3', 'value': 'three', 'caption': 'string3', 'desc': ''}, + ] + }] + } + class LocalMockWriter(mock_writer.MockWriter): + def WritePolicy(self, policy): + self.tester.assertEquals(policy['items'][0]['caption'], 'string1') + self.tester.assertEquals(policy['items'][1]['caption'], 'string2') + self.tester.assertEquals(policy['items'][2]['caption'], 'string3') + self.do_test(policy_data_mock, LocalMockWriter()) + + def testStringEnumTexts(self): + # Test that GUI messages are assigned correctly to string-enums + # (aka dropdown menus). + policy_data_mock = { + 'policy_definitions': [{ + 'name': 'Policy1', + 'type': 'string-enum-list', + 'caption': '', 'desc': '', + 'supported_on': [], + 'items': [ + {'name': 'item1', 'value': 'one', 'caption': 'string1', 'desc': ''}, + {'name': 'item2', 'value': 'two', 'caption': 'string2', 'desc': ''}, + {'name': 'item3', 'value': 'three', 'caption': 'string3', 'desc': ''}, + ] + }] + } + class LocalMockWriter(mock_writer.MockWriter): + def WritePolicy(self, policy): + self.tester.assertEquals(policy['items'][0]['caption'], 'string1') + self.tester.assertEquals(policy['items'][1]['caption'], 'string2') + self.tester.assertEquals(policy['items'][2]['caption'], 'string3') + self.do_test(policy_data_mock, LocalMockWriter()) + + def testPolicyFiltering(self): + # Test that policies are filtered correctly based on their annotations. + policy_data_mock = { + 'policy_definitions': [ + { + 'name': 'Group1', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [ + { + 'name': 'Group1Policy1', + 'type': 'string', + 'caption': '', + 'desc': '', + 'supported_on': [ + 'chrome.aaa:8-', 'chrome.bbb:8-', 'chrome.ccc:8-' + ] + }, + { + 'name': 'Group1Policy2', + 'type': 'string', + 'caption': '', + 'desc': '', + 'supported_on': ['chrome.ddd:8-'] + }, + ] + }, { + 'name': 'Group2', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [ + { + 'name': 'Group2Policy3', + 'type': 'string', + 'caption': '', + 'desc': '', + 'supported_on': ['chrome.eee:8-'] + }, + ] + }, { + 'name': 'SinglePolicy', + 'type': 'int', + 'caption': '', + 'desc': '', + 'supported_on': ['chrome.eee:8-'] + } + ] + } + # This writer accumulates the list of policies it is asked to write. + # This list is stored in the result_list member variable and can + # be used later for assertions. + class LocalMockWriter(mock_writer.MockWriter): + def __init__(self, platforms): + self.platforms = platforms + self.policy_name = None + self.result_list = [] + def BeginPolicyGroup(self, group): + self.group = group; + self.result_list.append('begin_' + group['name']) + def EndPolicyGroup(self): + self.result_list.append('end_group') + self.group = None + def WritePolicy(self, policy): + self.result_list.append(policy['name']) + def IsPolicySupported(self, policy): + # Call the original (non-mock) implementation of this method. + return template_writer.TemplateWriter.IsPolicySupported(self, policy) + + local_mock_writer = LocalMockWriter(['eee']) + self.do_test(policy_data_mock, local_mock_writer) + # Test that only policies of platform 'eee' were written: + self.assertEquals( + local_mock_writer.result_list, + ['begin_Group2', 'Group2Policy3', 'end_group', 'SinglePolicy']) + + local_mock_writer = LocalMockWriter(['ddd', 'bbb']) + self.do_test(policy_data_mock, local_mock_writer) + # Test that only policies of platforms 'ddd' and 'bbb' were written: + self.assertEquals( + local_mock_writer.result_list, + ['begin_Group1', 'Group1Policy1', 'Group1Policy2', 'end_group']) + + def testSortingInvoked(self): + # Tests that policy-sorting happens before passing policies to the writer. + policy_data = { + 'policy_definitions': [ + {'name': 'zp', 'type': 'string', 'supported_on': [], + 'caption': '', 'desc': ''}, + {'name': 'ap', 'type': 'string', 'supported_on': [], + 'caption': '', 'desc': ''}, + ] + } + class LocalMockWriter(mock_writer.MockWriter): + def __init__(self): + self.result_list = [] + def WritePolicy(self, policy): + self.result_list.append(policy['name']) + def Test(self): + self.tester.assertEquals( + self.result_list, + ['ap', 'zp']) + self.do_test(policy_data, LocalMockWriter()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/template_formatter.py b/tools/grit/grit/format/policy_templates/template_formatter.py new file mode 100755 index 0000000..53b84ec --- /dev/null +++ b/tools/grit/grit/format/policy_templates/template_formatter.py @@ -0,0 +1,73 @@ +#!/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. + + +import sys +from functools import partial + +from grit.format.policy_templates import policy_template_generator +from grit.format.policy_templates import writer_configuration +from grit.node import misc +from grit.node import structure + + +def GetFormatter(type): + return partial(_TemplateFormatter, + 'grit.format.policy_templates.writers.%s_writer' % type) + + +def _TemplateFormatter(writer_module_name, root, lang, output_dir): + '''Creates a template file corresponding to an node of the grit + tree. + + More precisely, processes the whole grit tree for a given node whose + type is one of adm, plist, plist_strings, admx, adml, doc, json, reg. + The result of processing is a policy template file with the given type and + language of the node. This function does the interfacing with + grit, but the actual template-generating work is done in + policy_template_generator.PolicyTemplateGenerator. + + Args: + writer_name: A string identifying the TemplateWriter subclass used + for generating the output. + root: the root node of the grit tree. + lang: the language of outputted text, e.g.: 'en' + output_dir: The output directory, currently unused here. + + Yields the text of the template file. + ''' + __import__(writer_module_name) + writer_module = sys.modules[writer_module_name] + config = writer_configuration.GetConfigurationForBuild(root.defines) + policy_data = _ParseGritNodes(root, lang) + policy_generator = \ + policy_template_generator.PolicyTemplateGenerator(config, policy_data) + writer = writer_module.GetWriter(config) + yield policy_generator.GetTemplateText(writer) + + +def _ParseGritNodes(root, lang): + '''Collects the necessary information from the grit tree: + the message strings and the policy definitions. + + Args: + root: The root of the grit tree. + lang: the language of outputted text, e.g.: 'en' + + Returns: + Policy data. + ''' + policy_data = None + for item in root.ActiveDescendants(): + with item: + if (isinstance(item, structure.StructureNode) and + item.attrs['type'] == 'policy_template_metafile'): + assert policy_data is None + json_text = item.gatherer.Translate( + lang, + pseudo_if_not_available=item.PseudoIsAllowed(), + fallback_to_english=item.ShouldFallbackToEnglish()) + policy_data = eval(json_text) + return policy_data diff --git a/tools/grit/grit/format/policy_templates/writer_configuration.py b/tools/grit/grit/format/policy_templates/writer_configuration.py new file mode 100755 index 0000000..db9613b --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writer_configuration.py @@ -0,0 +1,61 @@ +#!/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. + + +def GetConfigurationForBuild(defines): + '''Returns a configuration dictionary for the given build that contains + build-specific settings and information. + + Args: + defines: Definitions coming from the build system. + + Raises: + Exception: If 'defines' contains an unknown build-type. + ''' + # The prefix of key names in config determines which writer will use their + # corresponding values: + # win: Both ADM and ADMX. + # mac: Only plist. + # admx: Only ADMX. + # none/other: Used by all the writers. + if '_chromium' in defines: + config = { + 'build': 'chromium', + 'app_name': 'Chromium', + 'frame_name': 'Chromium Frame', + 'os_name': 'Chromium OS', + 'win_reg_mandatory_key_name': 'Software\\Policies\\Chromium', + 'win_reg_recommended_key_name': + 'Software\\Policies\\Chromium\\Recommended', + 'win_mandatory_category_path': ['chromium'], + 'win_recommended_category_path': ['chromium_recommended'], + 'admx_namespace': 'Chromium.Policies.Chromium', + 'admx_prefix': 'chromium', + 'linux_policy_path': '/etc/chromium/policies/', + } + elif '_google_chrome' in defines: + config = { + 'build': 'chrome', + 'app_name': 'Google Chrome', + 'frame_name': 'Google Chrome Frame', + 'os_name': 'Google Chrome OS', + 'win_reg_mandatory_key_name': 'Software\\Policies\\Google\\Chrome', + 'win_reg_recommended_key_name': + 'Software\\Policies\\Google\\Chrome\\Recommended', + 'win_mandatory_category_path': ['google', 'googlechrome'], + 'win_recommended_category_path': ['google', 'googlechrome_recommended'], + 'admx_namespace': 'Google.Policies.Chrome', + 'admx_prefix': 'chrome', + 'linux_policy_path': '/etc/opt/chrome/policies/', + } + else: + raise Exception('Unknown build') + if 'version' in defines: + config['version'] = defines['version'] + config['win_group_policy_class'] = 'Both' + config['win_supported_os'] = 'SUPPORTED_WINXPSP2' + if 'mac_bundle_id' in defines: + config['mac_bundle_id'] = defines['mac_bundle_id'] + return config diff --git a/tools/grit/grit/format/policy_templates/writers/__init__.py b/tools/grit/grit/format/policy_templates/writers/__init__.py new file mode 100755 index 0000000..fe6d139 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/__init__.py @@ -0,0 +1,10 @@ +#!/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. + +'''Module grit.format.policy_templates.writers +''' + +pass + diff --git a/tools/grit/grit/format/policy_templates/writers/adm_writer.py b/tools/grit/grit/format/policy_templates/writers/adm_writer.py new file mode 100755 index 0000000..91aeef6 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/adm_writer.py @@ -0,0 +1,274 @@ +#!/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. + + +from grit.format.policy_templates.writers import template_writer + + +NEWLINE = '\r\n' + + +def GetWriter(config): + '''Factory method for creating AdmWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return AdmWriter(['win'], config) + + +class IndentedStringBuilder: + '''Utility class for building text with indented lines.''' + + def __init__(self): + self.lines = [] + self.indent = '' + + def AddLine(self, string='', indent_diff=0): + '''Appends a string with indentation and a linebreak to |self.lines|. + + Args: + string: The string to print. + indent_diff: the difference of indentation of the printed line, + compared to the next/previous printed line. Increment occurs + after printing the line, while decrement occurs before that. + ''' + indent_diff *= 2 + if indent_diff < 0: + self.indent = self.indent[(-indent_diff):] + if string != '': + self.lines.append(self.indent + string) + else: + self.lines.append('') + if indent_diff > 0: + self.indent += ''.ljust(indent_diff) + + def AddLines(self, other): + '''Appends the content of another |IndentedStringBuilder| to |self.lines|. + Indentation of the added lines will be the sum of |self.indent| and + their original indentation. + + Args: + other: The buffer from which lines are copied. + ''' + for line in other.lines: + self.AddLine(line) + + def ToString(self): + '''Returns |self.lines| as text string.''' + return NEWLINE.join(self.lines) + + +class AdmWriter(template_writer.TemplateWriter): + '''Class for generating policy templates in Windows ADM format. + It is used by PolicyTemplateGenerator to write ADM files. + ''' + + TYPE_TO_INPUT = { + 'string': 'EDITTEXT', + 'int': 'NUMERIC', + 'string-enum': 'DROPDOWNLIST', + 'int-enum': 'DROPDOWNLIST', + 'list': 'LISTBOX', + 'string-enum-list': 'LISTBOX', + 'dict': 'EDITTEXT' + } + + def _Escape(self, string): + return string.replace('.', '_') + + def _AddGuiString(self, name, value): + # The |name| must be escaped. + assert name == self._Escape(name) + # Escape newlines in the value. + value = value.replace('\n', '\\n') + if name in self.strings_seen: + err = ('%s was added as "%s" and now added again as "%s"' % + (name, self.strings_seen[name], value)) + assert value == self.strings_seen[name], err + else: + self.strings_seen[name] = value + line = '%s="%s"' % (name, value) + self.strings.AddLine(line) + + def _WriteSupported(self, builder): + builder.AddLine('#if version >= 4', 1) + builder.AddLine('SUPPORTED !!SUPPORTED_WINXPSP2') + builder.AddLine('#endif', -1) + + def _WritePart(self, policy, key_name, builder): + '''Writes the PART ... END PART section of a policy. + + Args: + policy: The policy to write to the output. + key_name: The registry key backing the policy. + builder: Builder to append lines to. + ''' + policy_part_name = self._Escape(policy['name'] + '_Part') + self._AddGuiString(policy_part_name, policy['label']) + + # Print the PART ... END PART section: + builder.AddLine() + adm_type = self.TYPE_TO_INPUT[policy['type']] + builder.AddLine('PART !!%s %s' % (policy_part_name, adm_type), 1) + if policy['type'] in ('list', 'string-enum-list'): + # Note that the following line causes FullArmor ADMX Migrator to create + # corrupt ADMX files. Please use admx_writer to get ADMX files. + builder.AddLine('KEYNAME "%s\\%s"' % (key_name, policy['name'])) + builder.AddLine('VALUEPREFIX ""') + else: + builder.AddLine('VALUENAME "%s"' % policy['name']) + if policy['type'] == 'int': + # The default max for NUMERIC values is 9999 which is too small for us. + builder.AddLine('MIN 0 MAX 2000000000') + if policy['type'] in ('string', 'dict'): + # The default max for EDITTEXT values is 1023, which is too small for + # big JSON blobs and other string policies. + builder.AddLine('MAXLEN 1000000') + if policy['type'] in ('int-enum', 'string-enum'): + builder.AddLine('ITEMLIST', 1) + for item in policy['items']: + if policy['type'] == 'int-enum': + value_text = 'NUMERIC ' + str(item['value']) + else: + value_text = '"' + item['value'] + '"' + string_id = self._Escape(item['name'] + '_DropDown') + builder.AddLine('NAME !!%s VALUE %s' % (string_id, value_text)) + self._AddGuiString(string_id, item['caption']) + builder.AddLine('END ITEMLIST', -1) + builder.AddLine('END PART', -1) + + def _WritePolicy(self, policy, key_name, builder): + if policy['type'] == 'external': + # This type can only be set through cloud policy. + return + + policy_name = self._Escape(policy['name'] + '_Policy') + self._AddGuiString(policy_name, policy['caption']) + builder.AddLine('POLICY !!%s' % policy_name, 1) + self._WriteSupported(builder) + policy_explain_name = self._Escape(policy['name'] + '_Explain') + self._AddGuiString(policy_explain_name, policy['desc']) + builder.AddLine('EXPLAIN !!' + policy_explain_name) + + if policy['type'] == 'main': + builder.AddLine('VALUENAME "%s"' % policy['name']) + builder.AddLine('VALUEON NUMERIC 1') + builder.AddLine('VALUEOFF NUMERIC 0') + else: + self._WritePart(policy, key_name, builder) + + builder.AddLine('END POLICY', -1) + builder.AddLine() + + def WriteComment(self, comment): + self.lines.AddLine('; ' + comment) + + def WritePolicy(self, policy): + if self.CanBeMandatory(policy): + self._WritePolicy(policy, + self.config['win_reg_mandatory_key_name'], + self.policies) + + def WriteRecommendedPolicy(self, policy): + self._WritePolicy(policy, + self.config['win_reg_recommended_key_name'], + self.recommended_policies) + + def BeginPolicyGroup(self, group): + category_name = self._Escape(group['name'] + '_Category') + self._AddGuiString(category_name, group['caption']) + self.policies.AddLine('CATEGORY !!' + category_name, 1) + + def EndPolicyGroup(self): + self.policies.AddLine('END CATEGORY', -1) + self.policies.AddLine('') + + def BeginRecommendedPolicyGroup(self, group): + category_name = self._Escape(group['name'] + '_Category') + self._AddGuiString(category_name, group['caption']) + self.recommended_policies.AddLine('CATEGORY !!' + category_name, 1) + + def EndRecommendedPolicyGroup(self): + self.recommended_policies.AddLine('END CATEGORY', -1) + self.recommended_policies.AddLine('') + + def _CreateTemplate(self, category_path, key_name, policies): + '''Creates the whole ADM template except for the [Strings] section, and + returns it as an |IndentedStringBuilder|. + + Args: + category_path: List of strings representing the category path. + key_name: Main registry key backing the policies. + policies: ADM code for all the policies in an |IndentedStringBuilder|. + ''' + lines = IndentedStringBuilder() + for part in category_path: + lines.AddLine('CATEGORY !!' + part, 1) + lines.AddLine('KEYNAME "%s"' % key_name) + lines.AddLine() + + lines.AddLines(policies) + + for part in category_path: + lines.AddLine('END CATEGORY', -1) + lines.AddLine() + + return lines + + def BeginTemplate(self): + if self._GetChromiumVersionString() is not None: + self.WriteComment(self.config['build'] + ' version: ' + \ + self._GetChromiumVersionString()) + self._AddGuiString(self.config['win_supported_os'], + self.messages['win_supported_winxpsp2']['text']) + category_path = self.config['win_mandatory_category_path'] + recommended_category_path = self.config['win_recommended_category_path'] + recommended_name = '%s - %s' % \ + (self.config['app_name'], self.messages['doc_recommended']['text']) + if self.config['build'] == 'chrome': + self._AddGuiString(category_path[0], 'Google') + self._AddGuiString(category_path[1], self.config['app_name']) + self._AddGuiString(recommended_category_path[1], recommended_name) + elif self.config['build'] == 'chromium': + self._AddGuiString(category_path[0], self.config['app_name']) + self._AddGuiString(recommended_category_path[0], recommended_name) + # All the policies will be written into self.policies. + # The final template text will be assembled into self.lines by + # self.EndTemplate(). + + def EndTemplate(self): + # Copy policies into self.lines. + policy_class = self.config['win_group_policy_class'].upper() + for class_name in ['MACHINE', 'USER']: + if policy_class != 'BOTH' and policy_class != class_name: + continue + self.lines.AddLine('CLASS ' + class_name, 1) + self.lines.AddLines(self._CreateTemplate( + self.config['win_mandatory_category_path'], + self.config['win_reg_mandatory_key_name'], + self.policies)) + self.lines.AddLines(self._CreateTemplate( + self.config['win_recommended_category_path'], + self.config['win_reg_recommended_key_name'], + self.recommended_policies)) + self.lines.AddLine('', -1) + # Copy user strings into self.lines. + self.lines.AddLine('[Strings]') + self.lines.AddLines(self.strings) + + def Init(self): + # String buffer for building the whole ADM file. + self.lines = IndentedStringBuilder() + # String buffer for building the strings section of the ADM file. + self.strings = IndentedStringBuilder() + # Map of strings seen, to avoid duplicates. + self.strings_seen = {} + # String buffer for building the policies of the ADM file. + self.policies = IndentedStringBuilder() + # String buffer for building the recommended policies of the ADM file. + self.recommended_policies = IndentedStringBuilder() + + def GetTemplateText(self): + return self.lines.ToString() diff --git a/tools/grit/grit/format/policy_templates/writers/adm_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/adm_writer_unittest.py new file mode 100755 index 0000000..c5c64f7 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/adm_writer_unittest.py @@ -0,0 +1,1127 @@ +#!/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.policy_templates.writers.adm_writer''' + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest + +from grit.format.policy_templates.writers import writer_unittest_common + + +class AdmWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests for AdmWriter.''' + + def ConstructOutput(self, classes, body, strings): + result = [] + for clazz in classes: + result.append('CLASS ' + clazz) + result.append(body) + result.append(strings) + return ''.join(result) + + def CompareOutputs(self, output, expected_output): + '''Compares the output of the adm_writer with its expected output. + + Args: + output: The output of the adm writer as returned by grit. + expected_output: The expected output. + + Raises: + AssertionError: if the two strings are not equivalent. + ''' + self.assertEquals( + output.strip(), + expected_output.strip().replace('\n', '\r\n')) + + def testEmpty(self): + # Test PListWriter in case of empty polices. + grd = self.PrepareTest(''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least "Windows 3.11', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_chromium': '1',}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least "Windows 3.11" +chromium="Chromium" +chromium_recommended="Chromium - Recommended"''') + self.CompareOutputs(output, expected_output) + + def testVersionAnnotation(self): + # Test PListWriter in case of empty polices. + grd = self.PrepareTest(''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least "Windows 3.11', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput( + grd, 'fr', {'_chromium': '1', 'version':'39.0.0.0'}, 'adm', 'en') + expected_output = '; chromium version: 39.0.0.0\n' + \ + self.ConstructOutput(['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least "Windows 3.11" +chromium="Chromium" +chromium_recommended="Chromium - Recommended"''') + self.CompareOutputs(output, expected_output) + + def testMainPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'MainPolicy', + 'type': 'main', + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + 'caption': 'Caption of main.', + 'desc': 'Description of main.', + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.12', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_google_chrome' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!google + CATEGORY !!googlechrome + KEYNAME "Software\\Policies\\Google\\Chrome" + + POLICY !!MainPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!MainPolicy_Explain + VALUENAME "MainPolicy" + VALUEON NUMERIC 1 + VALUEOFF NUMERIC 0 + END POLICY + + END CATEGORY + END CATEGORY + + CATEGORY !!google + CATEGORY !!googlechrome_recommended + KEYNAME "Software\\Policies\\Google\\Chrome\\Recommended" + + POLICY !!MainPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!MainPolicy_Explain + VALUENAME "MainPolicy" + VALUEON NUMERIC 1 + VALUEOFF NUMERIC 0 + END POLICY + + END CATEGORY + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.12" +google="Google" +googlechrome="Google Chrome" +googlechrome_recommended="Google Chrome - Recommended" +MainPolicy_Policy="Caption of main." +MainPolicy_Explain="Description of main."''') + self.CompareOutputs(output, expected_output) + + def testMainPolicyRecommendedOnly(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'MainPolicy', + 'type': 'main', + 'supported_on': ['chrome.win:8-'], + 'features': { + 'can_be_recommended': True, + 'can_be_mandatory': False + }, + 'caption': 'Caption of main.', + 'desc': 'Description of main.', + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.12', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_google_chrome' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!google + CATEGORY !!googlechrome + KEYNAME "Software\\Policies\\Google\\Chrome" + + END CATEGORY + END CATEGORY + + CATEGORY !!google + CATEGORY !!googlechrome_recommended + KEYNAME "Software\\Policies\\Google\\Chrome\\Recommended" + + POLICY !!MainPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!MainPolicy_Explain + VALUENAME "MainPolicy" + VALUEON NUMERIC 1 + VALUEOFF NUMERIC 0 + END POLICY + + END CATEGORY + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.12" +google="Google" +googlechrome="Google Chrome" +googlechrome_recommended="Google Chrome - Recommended" +MainPolicy_Policy="Caption of main." +MainPolicy_Explain="Description of main."''') + self.CompareOutputs(output, expected_output) + + def testStringPolicy(self): + # Tests a policy group with a single policy of type 'string'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'StringPolicy', + 'type': 'string', + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + 'desc': """Description of group. +With a newline.""", + 'caption': 'Caption of policy.', + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.13', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + POLICY !!StringPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!StringPolicy_Explain + + PART !!StringPolicy_Part EDITTEXT + VALUENAME "StringPolicy" + MAXLEN 1000000 + END PART + END POLICY + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + POLICY !!StringPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!StringPolicy_Explain + + PART !!StringPolicy_Part EDITTEXT + VALUENAME "StringPolicy" + MAXLEN 1000000 + END PART + END POLICY + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.13" +chromium="Chromium" +chromium_recommended="Chromium - Recommended" +StringPolicy_Policy="Caption of policy." +StringPolicy_Explain="Description of group.\\nWith a newline." +StringPolicy_Part="Caption of policy." +''') + self.CompareOutputs(output, expected_output) + + def testIntPolicy(self): + # Tests a policy group with a single policy of type 'string'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'IntPolicy', + 'type': 'int', + 'caption': 'Caption of policy.', + 'features': { 'can_be_recommended': True }, + 'desc': 'Description of policy.', + 'supported_on': ['chrome.win:8-'] + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.13', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + POLICY !!IntPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!IntPolicy_Explain + + PART !!IntPolicy_Part NUMERIC + VALUENAME "IntPolicy" + MIN 0 MAX 2000000000 + END PART + END POLICY + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + POLICY !!IntPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!IntPolicy_Explain + + PART !!IntPolicy_Part NUMERIC + VALUENAME "IntPolicy" + MIN 0 MAX 2000000000 + END PART + END POLICY + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.13" +chromium="Chromium" +chromium_recommended="Chromium - Recommended" +IntPolicy_Policy="Caption of policy." +IntPolicy_Explain="Description of policy." +IntPolicy_Part="Caption of policy." +''') + self.CompareOutputs(output, expected_output) + + def testIntEnumPolicy(self): + # Tests a policy group with a single policy of type 'int-enum'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumPolicy', + 'type': 'int-enum', + 'items': [ + { + 'name': 'ProxyServerDisabled', + 'value': 0, + 'caption': 'Option1', + }, + { + 'name': 'ProxyServerAutoDetect', + 'value': 1, + 'caption': 'Option2', + }, + ], + 'desc': 'Description of policy.', + 'caption': 'Caption of policy.', + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.14', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_google_chrome': '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!google + CATEGORY !!googlechrome + KEYNAME "Software\\Policies\\Google\\Chrome" + + POLICY !!EnumPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!EnumPolicy_Explain + + PART !!EnumPolicy_Part DROPDOWNLIST + VALUENAME "EnumPolicy" + ITEMLIST + NAME !!ProxyServerDisabled_DropDown VALUE NUMERIC 0 + NAME !!ProxyServerAutoDetect_DropDown VALUE NUMERIC 1 + END ITEMLIST + END PART + END POLICY + + END CATEGORY + END CATEGORY + + CATEGORY !!google + CATEGORY !!googlechrome_recommended + KEYNAME "Software\\Policies\\Google\\Chrome\\Recommended" + + POLICY !!EnumPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!EnumPolicy_Explain + + PART !!EnumPolicy_Part DROPDOWNLIST + VALUENAME "EnumPolicy" + ITEMLIST + NAME !!ProxyServerDisabled_DropDown VALUE NUMERIC 0 + NAME !!ProxyServerAutoDetect_DropDown VALUE NUMERIC 1 + END ITEMLIST + END PART + END POLICY + + END CATEGORY + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.14" +google="Google" +googlechrome="Google Chrome" +googlechrome_recommended="Google Chrome - Recommended" +EnumPolicy_Policy="Caption of policy." +EnumPolicy_Explain="Description of policy." +EnumPolicy_Part="Caption of policy." +ProxyServerDisabled_DropDown="Option1" +ProxyServerAutoDetect_DropDown="Option2" +''') + self.CompareOutputs(output, expected_output) + + def testStringEnumPolicy(self): + # Tests a policy group with a single policy of type 'int-enum'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumPolicy', + 'type': 'string-enum', + 'caption': 'Caption of policy.', + 'desc': 'Description of policy.', + 'items': [ + {'name': 'ProxyServerDisabled', 'value': 'one', + 'caption': 'Option1'}, + {'name': 'ProxyServerAutoDetect', 'value': 'two', + 'caption': 'Option2'}, + ], + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.14', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_google_chrome': '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!google + CATEGORY !!googlechrome + KEYNAME "Software\\Policies\\Google\\Chrome" + + POLICY !!EnumPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!EnumPolicy_Explain + + PART !!EnumPolicy_Part DROPDOWNLIST + VALUENAME "EnumPolicy" + ITEMLIST + NAME !!ProxyServerDisabled_DropDown VALUE "one" + NAME !!ProxyServerAutoDetect_DropDown VALUE "two" + END ITEMLIST + END PART + END POLICY + + END CATEGORY + END CATEGORY + + CATEGORY !!google + CATEGORY !!googlechrome_recommended + KEYNAME "Software\\Policies\\Google\\Chrome\\Recommended" + + POLICY !!EnumPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!EnumPolicy_Explain + + PART !!EnumPolicy_Part DROPDOWNLIST + VALUENAME "EnumPolicy" + ITEMLIST + NAME !!ProxyServerDisabled_DropDown VALUE "one" + NAME !!ProxyServerAutoDetect_DropDown VALUE "two" + END ITEMLIST + END PART + END POLICY + + END CATEGORY + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.14" +google="Google" +googlechrome="Google Chrome" +googlechrome_recommended="Google Chrome - Recommended" +EnumPolicy_Policy="Caption of policy." +EnumPolicy_Explain="Description of policy." +EnumPolicy_Part="Caption of policy." +ProxyServerDisabled_DropDown="Option1" +ProxyServerAutoDetect_DropDown="Option2" +''') + self.CompareOutputs(output, expected_output) + + def testListPolicy(self): + # Tests a policy group with a single policy of type 'list'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'ListPolicy', + 'type': 'list', + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + 'desc': """Description of list policy. +With a newline.""", + 'caption': 'Caption of list policy.', + 'label': 'Label of list policy.' + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.15', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + }, + }''') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + POLICY !!ListPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!ListPolicy_Explain + + PART !!ListPolicy_Part LISTBOX + KEYNAME "Software\\Policies\\Chromium\\ListPolicy" + VALUEPREFIX "" + END PART + END POLICY + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + POLICY !!ListPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!ListPolicy_Explain + + PART !!ListPolicy_Part LISTBOX + KEYNAME "Software\\Policies\\Chromium\\Recommended\\ListPolicy" + VALUEPREFIX "" + END PART + END POLICY + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.15" +chromium="Chromium" +chromium_recommended="Chromium - Recommended" +ListPolicy_Policy="Caption of list policy." +ListPolicy_Explain="Description of list policy.\\nWith a newline." +ListPolicy_Part="Label of list policy." +''') + self.CompareOutputs(output, expected_output) + + def testStringEnumListPolicy(self): + # Tests a policy group with a single policy of type 'string-enum-list'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'ListPolicy', + 'type': 'string-enum-list', + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + 'desc': """Description of list policy. +With a newline.""", + 'items': [ + {'name': 'ProxyServerDisabled', 'value': 'one', + 'caption': 'Option1'}, + {'name': 'ProxyServerAutoDetect', 'value': 'two', + 'caption': 'Option2'}, + ], + 'caption': 'Caption of list policy.', + 'label': 'Label of list policy.' + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.15', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + }, + }''') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + POLICY !!ListPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!ListPolicy_Explain + + PART !!ListPolicy_Part LISTBOX + KEYNAME "Software\\Policies\\Chromium\\ListPolicy" + VALUEPREFIX "" + END PART + END POLICY + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + POLICY !!ListPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!ListPolicy_Explain + + PART !!ListPolicy_Part LISTBOX + KEYNAME "Software\\Policies\\Chromium\\Recommended\\ListPolicy" + VALUEPREFIX "" + END PART + END POLICY + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.15" +chromium="Chromium" +chromium_recommended="Chromium - Recommended" +ListPolicy_Policy="Caption of list policy." +ListPolicy_Explain="Description of list policy.\\nWith a newline." +ListPolicy_Part="Label of list policy." +''') + self.CompareOutputs(output, expected_output) + + def testDictionaryPolicy(self): + # Tests a policy group with a single policy of type 'dict'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'DictionaryPolicy', + 'type': 'dict', + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + 'desc': 'Description of group.', + 'caption': 'Caption of policy.', + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.13', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + POLICY !!DictionaryPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!DictionaryPolicy_Explain + + PART !!DictionaryPolicy_Part EDITTEXT + VALUENAME "DictionaryPolicy" + MAXLEN 1000000 + END PART + END POLICY + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + POLICY !!DictionaryPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!DictionaryPolicy_Explain + + PART !!DictionaryPolicy_Part EDITTEXT + VALUENAME "DictionaryPolicy" + MAXLEN 1000000 + END PART + END POLICY + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.13" +chromium="Chromium" +chromium_recommended="Chromium - Recommended" +DictionaryPolicy_Policy="Caption of policy." +DictionaryPolicy_Explain="Description of group." +DictionaryPolicy_Part="Caption of policy." +''') + self.CompareOutputs(output, expected_output) + + def testNonSupportedPolicy(self): + # Tests a policy that is not supported on Windows, so it shouldn't + # be included in the ADM file. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'NonWinGroup', + 'type': 'group', + 'policies': [{ + 'name': 'NonWinPolicy', + 'type': 'list', + 'supported_on': ['chrome.linux:8-', 'chrome.mac:8-'], + 'caption': 'Caption of list policy.', + 'desc': 'Desc of list policy.', + }], + 'caption': 'Group caption.', + 'desc': 'Group description.', + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.16', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.16" +chromium="Chromium" +chromium_recommended="Chromium - Recommended" +''') + self.CompareOutputs(output, expected_output) + + def testNonRecommendedPolicy(self): + # Tests a policy that is not recommended, so it should be included. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'MainPolicy', + 'type': 'main', + 'supported_on': ['chrome.win:8-'], + 'caption': 'Caption of main.', + 'desc': 'Description of main.', + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.12', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_google_chrome' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!google + CATEGORY !!googlechrome + KEYNAME "Software\\Policies\\Google\\Chrome" + + POLICY !!MainPolicy_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!MainPolicy_Explain + VALUENAME "MainPolicy" + VALUEON NUMERIC 1 + VALUEOFF NUMERIC 0 + END POLICY + + END CATEGORY + END CATEGORY + + CATEGORY !!google + CATEGORY !!googlechrome_recommended + KEYNAME "Software\\Policies\\Google\\Chrome\\Recommended" + + END CATEGORY + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.12" +google="Google" +googlechrome="Google Chrome" +googlechrome_recommended="Google Chrome - Recommended" +MainPolicy_Policy="Caption of main." +MainPolicy_Explain="Description of main."''') + self.CompareOutputs(output, expected_output) + + def testPolicyGroup(self): + # Tests a policy group that has more than one policies. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'Group1', + 'type': 'group', + 'desc': 'Description of group.', + 'caption': 'Caption of group.', + 'policies': [{ + 'name': 'Policy1', + 'type': 'list', + 'supported_on': ['chrome.win:8-'], + 'features': { 'can_be_recommended': True }, + 'caption': 'Caption of policy1.', + 'desc': """Description of policy1. +With a newline.""" + },{ + 'name': 'Policy2', + 'type': 'string', + 'supported_on': ['chrome.win:8-'], + 'caption': 'Caption of policy2.', + 'desc': """Description of policy2. +With a newline.""" + }], + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.16', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!chromium + KEYNAME "Software\\Policies\\Chromium" + + CATEGORY !!Group1_Category + POLICY !!Policy1_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!Policy1_Explain + + PART !!Policy1_Part LISTBOX + KEYNAME "Software\\Policies\\Chromium\\Policy1" + VALUEPREFIX "" + END PART + END POLICY + + POLICY !!Policy2_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!Policy2_Explain + + PART !!Policy2_Part EDITTEXT + VALUENAME "Policy2" + MAXLEN 1000000 + END PART + END POLICY + + END CATEGORY + + END CATEGORY + + CATEGORY !!chromium_recommended + KEYNAME "Software\\Policies\\Chromium\\Recommended" + + CATEGORY !!Group1_Category + POLICY !!Policy1_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!Policy1_Explain + + PART !!Policy1_Part LISTBOX + KEYNAME "Software\\Policies\\Chromium\\Recommended\\Policy1" + VALUEPREFIX "" + END PART + END POLICY + + END CATEGORY + + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.16" +chromium="Chromium" +chromium_recommended="Chromium - Recommended" +Group1_Category="Caption of group." +Policy1_Policy="Caption of policy1." +Policy1_Explain="Description of policy1.\\nWith a newline." +Policy1_Part="Caption of policy1." +Policy2_Policy="Caption of policy2." +Policy2_Explain="Description of policy2.\\nWith a newline." +Policy2_Part="Caption of policy2." +''') + self.CompareOutputs(output, expected_output) + + def testDuplicatedStringEnumPolicy(self): + # Verifies that duplicated enum constants get merged, and that + # string constants get escaped. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumPolicy.A', + 'type': 'string-enum', + 'caption': 'Caption of policy A.', + 'desc': 'Description of policy A.', + 'items': [ + {'name': 'tls1.2', 'value': 'tls1.2', 'caption': 'tls1.2' }, + ], + 'supported_on': ['chrome.win:39-'], + }, + { + 'name': 'EnumPolicy.B', + 'type': 'string-enum', + 'caption': 'Caption of policy B.', + 'desc': 'Description of policy B.', + 'items': [ + {'name': 'tls1.2', 'value': 'tls1.2', 'caption': 'tls1.2' }, + ], + 'supported_on': ['chrome.win:39-'], + }, + ], + 'placeholders': [], + 'messages': { + 'win_supported_winxpsp2': { + 'text': 'At least Windows 3.14', 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', 'desc': 'bleh' + } + } + }''') + output = self.GetOutput(grd, 'fr', {'_google_chrome': '1'}, 'adm', 'en') + expected_output = self.ConstructOutput( + ['MACHINE', 'USER'], ''' + CATEGORY !!google + CATEGORY !!googlechrome + KEYNAME "Software\\Policies\\Google\\Chrome" + + POLICY !!EnumPolicy_A_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!EnumPolicy_A_Explain + + PART !!EnumPolicy_A_Part DROPDOWNLIST + VALUENAME "EnumPolicy.A" + ITEMLIST + NAME !!tls1_2_DropDown VALUE "tls1.2" + END ITEMLIST + END PART + END POLICY + + POLICY !!EnumPolicy_B_Policy + #if version >= 4 + SUPPORTED !!SUPPORTED_WINXPSP2 + #endif + EXPLAIN !!EnumPolicy_B_Explain + + PART !!EnumPolicy_B_Part DROPDOWNLIST + VALUENAME "EnumPolicy.B" + ITEMLIST + NAME !!tls1_2_DropDown VALUE "tls1.2" + END ITEMLIST + END PART + END POLICY + + END CATEGORY + END CATEGORY + + CATEGORY !!google + CATEGORY !!googlechrome_recommended + KEYNAME "Software\\Policies\\Google\\Chrome\\Recommended" + + END CATEGORY + END CATEGORY + + +''', '''[Strings] +SUPPORTED_WINXPSP2="At least Windows 3.14" +google="Google" +googlechrome="Google Chrome" +googlechrome_recommended="Google Chrome - Recommended" +EnumPolicy_A_Policy="Caption of policy A." +EnumPolicy_A_Explain="Description of policy A." +EnumPolicy_A_Part="Caption of policy A." +tls1_2_DropDown="tls1.2" +EnumPolicy_B_Policy="Caption of policy B." +EnumPolicy_B_Explain="Description of policy B." +EnumPolicy_B_Part="Caption of policy B." +''') + self.CompareOutputs(output, expected_output) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/adml_writer.py b/tools/grit/grit/format/policy_templates/writers/adml_writer.py new file mode 100755 index 0000000..64f8562 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/adml_writer.py @@ -0,0 +1,186 @@ +#!/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. + +from xml.dom import minidom +from grit.format.policy_templates.writers import xml_formatted_writer + + +def GetWriter(config): + '''Factory method for instanciating the ADMLWriter. Every Writer needs a + GetWriter method because the TemplateFormatter uses this method to + instantiate a Writer. + ''' + return ADMLWriter(['win'], config) + + +class ADMLWriter(xml_formatted_writer.XMLFormattedWriter): + ''' Class for generating an ADML policy template. It is used by the + PolicyTemplateGenerator to write the ADML file. + ''' + + # DOM root node of the generated ADML document. + _doc = None + + # The string-table contains all ADML "string" elements. + _string_table_elem = None + + # The presentation-table is the container for presentation elements, that + # describe the presentation of Policy-Groups and Policies. + _presentation_table_elem = None + + def _AddString(self, id, text): + ''' Adds an ADML "string" element to _string_table_elem. The following + ADML snippet contains an example: + + $(text) + + Args: + id: ID of the newly created "string" element. + text: Value of the newly created "string" element. + ''' + id = id.replace('.', '_') + if id in self.strings_seen: + assert text == self.strings_seen[id] + else: + self.strings_seen[id] = text + string_elem = self.AddElement( + self._string_table_elem, 'string', {'id': id}) + string_elem.appendChild(self._doc.createTextNode(text)) + + def WritePolicy(self, policy): + '''Generates the ADML elements for a Policy. + + ... + $(caption) + $(description) + + + + ... + + + + Args: + policy: The Policy to generate ADML elements for. + ''' + policy_type = policy['type'] + policy_name = policy['name'] + if 'caption' in policy: + policy_caption = policy['caption'] + else: + policy_caption = policy_name + if 'desc' in policy: + policy_description = policy['desc'] + else: + policy_description = policy_name + if 'label' in policy: + policy_label = policy['label'] + else: + policy_label = policy_name + + self._AddString(policy_name, policy_caption) + self._AddString(policy_name + '_Explain', policy_description) + presentation_elem = self.AddElement( + self._presentation_table_elem, 'presentation', {'id': policy_name}) + + if policy_type == 'main': + pass + elif policy_type in ('string', 'dict'): + # 'dict' policies are configured as JSON-encoded strings on Windows. + textbox_elem = self.AddElement(presentation_elem, 'textBox', + {'refId': policy_name}) + label_elem = self.AddElement(textbox_elem, 'label') + label_elem.appendChild(self._doc.createTextNode(policy_label)) + elif policy_type == 'int': + textbox_elem = self.AddElement(presentation_elem, 'decimalTextBox', + {'refId': policy_name}) + textbox_elem.appendChild(self._doc.createTextNode(policy_label + ':')) + elif policy_type in ('int-enum', 'string-enum'): + for item in policy['items']: + self._AddString(item['name'], item['caption']) + dropdownlist_elem = self.AddElement(presentation_elem, 'dropdownList', + {'refId': policy_name}) + dropdownlist_elem.appendChild(self._doc.createTextNode(policy_label)) + elif policy_type in ('list', 'string-enum-list'): + self._AddString(policy_name + 'Desc', policy_caption) + listbox_elem = self.AddElement(presentation_elem, 'listBox', + {'refId': policy_name + 'Desc'}) + listbox_elem.appendChild(self._doc.createTextNode(policy_label)) + elif policy_type == 'group': + pass + elif policy_type == 'external': + # This type can only be set through cloud policy. + pass + else: + raise Exception('Unknown policy type %s.' % policy_type) + + def BeginPolicyGroup(self, group): + '''Generates ADML elements for a Policy-Group. For each Policy-Group two + ADML "string" elements are added to the string-table. One contains the + caption of the Policy-Group and the other a description. A Policy-Group also + requires an ADML "presentation" element that must be added to the + presentation-table. The "presentation" element is the container for the + elements that define the visual presentation of the Policy-Goup's Policies. + The following ADML snippet shows an example: + + Args: + group: The Policy-Group to generate ADML elements for. + ''' + # Add ADML "string" elements to the string-table that are required by a + # Policy-Group. + self._AddString(group['name'] + '_group', group['caption']) + + def _AddBaseStrings(self, build): + ''' Adds ADML "string" elements to the string-table that are referenced by + the ADMX file but not related to any specific Policy-Group or Policy. + ''' + self._AddString(self.config['win_supported_os'], + self.messages['win_supported_winxpsp2']['text']) + recommended_name = '%s - %s' % \ + (self.config['app_name'], self.messages['doc_recommended']['text']) + if build == 'chrome': + self._AddString(self.config['win_mandatory_category_path'][0], + 'Google') + self._AddString(self.config['win_mandatory_category_path'][1], + self.config['app_name']) + self._AddString(self.config['win_recommended_category_path'][1], + recommended_name) + elif build == 'chromium': + self._AddString(self.config['win_mandatory_category_path'][0], + self.config['app_name']) + self._AddString(self.config['win_recommended_category_path'][0], + recommended_name) + + def BeginTemplate(self): + dom_impl = minidom.getDOMImplementation('') + self._doc = dom_impl.createDocument(None, 'policyDefinitionResources', + None) + if self._GetChromiumVersionString() is not None: + self.AddComment(self._doc.documentElement, self.config['build'] + \ + ' version: ' + self._GetChromiumVersionString()) + policy_definitions_resources_elem = self._doc.documentElement + policy_definitions_resources_elem.attributes['revision'] = '1.0' + policy_definitions_resources_elem.attributes['schemaVersion'] = '1.0' + + self.AddElement(policy_definitions_resources_elem, 'displayName') + self.AddElement(policy_definitions_resources_elem, 'description') + resources_elem = self.AddElement(policy_definitions_resources_elem, + 'resources') + self._string_table_elem = self.AddElement(resources_elem, 'stringTable') + self._AddBaseStrings(self.config['build']) + self._presentation_table_elem = self.AddElement(resources_elem, + 'presentationTable') + + def Init(self): + # Map of all strings seen. + self.strings_seen = {} + + def GetTemplateText(self): + # Using "toprettyxml()" confuses the Windows Group Policy Editor + # (gpedit.msc) because it interprets whitespace characters in text between + # the "string" tags. This prevents gpedit.msc from displaying the category + # names correctly. + # TODO(markusheintz): Find a better formatting that works with gpedit. + return self._doc.toxml() diff --git a/tools/grit/grit/format/policy_templates/writers/adml_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/adml_writer_unittest.py new file mode 100755 index 0000000..08bec58 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/adml_writer_unittest.py @@ -0,0 +1,437 @@ +#!/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. + + +"""Unittests for grit.format.policy_templates.writers.adml_writer.""" + + +import os +import sys +import unittest +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + + +from grit.format.policy_templates.writers import adml_writer +from grit.format.policy_templates.writers import xml_writer_base_unittest + + +class AdmlWriterUnittest(xml_writer_base_unittest.XmlWriterBaseTest): + + def setUp(self): + config = { + 'app_name': 'test', + 'build': 'test', + 'win_supported_os': 'SUPPORTED_TESTOS', + } + self.writer = adml_writer.GetWriter(config) + self.writer.messages = { + 'win_supported_winxpsp2': { + 'text': 'Supported on Test OS or higher', + 'desc': 'blah' + }, + 'doc_recommended': { + 'text': 'Recommended', + 'desc': 'bleh' + }, + } + self.writer.Init() + + def _InitWriterForAddingPolicyGroups(self, writer): + '''Initialize the writer for adding policy groups. This method must be + called before the method "BeginPolicyGroup" can be called. It initializes + attributes of the writer. + ''' + writer.BeginTemplate() + + def _InitWriterForAddingPolicies(self, writer, policy): + '''Initialize the writer for adding policies. This method must be + called before the method "WritePolicy" can be called. It initializes + attributes of the writer. + ''' + self._InitWriterForAddingPolicyGroups(writer) + policy_group = { + 'name': 'PolicyGroup', + 'caption': 'Test Caption', + 'desc': 'This is the test description of the test policy group.', + 'policies': policy, + } + writer.BeginPolicyGroup(policy_group) + + string_elements = \ + self.writer._string_table_elem.getElementsByTagName('string') + for elem in string_elements: + self.writer._string_table_elem.removeChild(elem) + + def testEmpty(self): + self.writer.BeginTemplate() + self.writer.EndTemplate() + output = self.writer.GetTemplateText() + expected_output = ( + '' + 'Supported on' + ' Test OS or higher' + '') + self.AssertXMLEquals(output, expected_output) + + def testVersionAnnotation(self): + self.writer.config['version'] = '39.0.0.0' + self.writer.BeginTemplate() + self.writer.EndTemplate() + output = self.writer.GetTemplateText() + expected_output = ( + '' + '' + 'Supported on' + ' Test OS or higher' + '') + self.AssertXMLEquals(output, expected_output) + + def testPolicyGroup(self): + empty_policy_group = { + 'name': 'PolicyGroup', + 'caption': 'Test Group Caption', + 'desc': 'This is the test description of the test policy group.', + 'policies': [ + {'name': 'PolicyStub2', + 'type': 'main'}, + {'name': 'PolicyStub1', + 'type': 'main'}, + ], + } + self._InitWriterForAddingPolicyGroups(self.writer) + self.writer.BeginPolicyGroup(empty_policy_group) + self.writer.EndPolicyGroup + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + '' + 'Supported on Test OS or higher\n' + 'Test Group Caption') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = '' + self.AssertXMLEquals(output, expected_output) + + def testMainPolicy(self): + main_policy = { + 'name': 'DummyMainPolicy', + 'type': 'main', + 'caption': 'Main policy caption', + 'desc': 'Main policy test description.' + } + self. _InitWriterForAddingPolicies(self.writer, main_policy) + self.writer.WritePolicy(main_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'Main policy caption\n' + '' + 'Main policy test description.') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = '' + self.AssertXMLEquals(output, expected_output) + + def testStringPolicy(self): + string_policy = { + 'name': 'StringPolicyStub', + 'type': 'string', + 'caption': 'String policy caption', + 'label': 'String policy label', + 'desc': 'This is a test description.', + } + self. _InitWriterForAddingPolicies(self.writer, string_policy) + self.writer.WritePolicy(string_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'String policy caption\n' + '' + 'This is a test description.') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testIntPolicy(self): + int_policy = { + 'name': 'IntPolicyStub', + 'type': 'int', + 'caption': 'Int policy caption', + 'label': 'Int policy label', + 'desc': 'This is a test description.', + } + self. _InitWriterForAddingPolicies(self.writer, int_policy) + self.writer.WritePolicy(int_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'Int policy caption\n' + '' + 'This is a test description.') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' ' + 'Int policy label:\n' + '') + self.AssertXMLEquals(output, expected_output) + + def testIntEnumPolicy(self): + enum_policy = { + 'name': 'EnumPolicyStub', + 'type': 'int-enum', + 'caption': 'Enum policy caption', + 'label': 'Enum policy label', + 'desc': 'This is a test description.', + 'items': [ + { + 'name': 'item 1', + 'value': 1, + 'caption': 'Caption Item 1', + }, + { + 'name': 'item 2', + 'value': 2, + 'caption': 'Caption Item 2', + }, + ], + } + self. _InitWriterForAddingPolicies(self.writer, enum_policy) + self.writer.WritePolicy(enum_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'Enum policy caption\n' + '' + 'This is a test description.\n' + 'Caption Item 1\n' + 'Caption Item 2') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' ' + 'Enum policy label\n' + '') + self.AssertXMLEquals(output, expected_output) + + def testStringEnumPolicy(self): + enum_policy = { + 'name': 'EnumPolicyStub', + 'type': 'string-enum', + 'caption': 'Enum policy caption', + 'label': 'Enum policy label', + 'desc': 'This is a test description.', + 'items': [ + { + 'name': 'item 1', + 'value': 'value 1', + 'caption': 'Caption Item 1', + }, + { + 'name': 'item 2', + 'value': 'value 2', + 'caption': 'Caption Item 2', + }, + ], + } + self. _InitWriterForAddingPolicies(self.writer, enum_policy) + self.writer.WritePolicy(enum_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'Enum policy caption\n' + '' + 'This is a test description.\n' + 'Caption Item 1\n' + 'Caption Item 2') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' ' + 'Enum policy label\n' + '') + self.AssertXMLEquals(output, expected_output) + + def testListPolicy(self): + list_policy = { + 'name': 'ListPolicyStub', + 'type': 'list', + 'caption': 'List policy caption', + 'label': 'List policy label', + 'desc': 'This is a test description.', + } + self. _InitWriterForAddingPolicies(self.writer, list_policy) + self.writer.WritePolicy(list_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'List policy caption\n' + '' + 'This is a test description.\n' + 'List policy caption') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' List policy label\n' + '') + self.AssertXMLEquals(output, expected_output) + + def testStringEnumListPolicy(self): + list_policy = { + 'name': 'ListPolicyStub', + 'type': 'string-enum-list', + 'caption': 'List policy caption', + 'label': 'List policy label', + 'desc': 'This is a test description.', + 'items': [ + { + 'name': 'item 1', + 'value': 'value 1', + 'caption': 'Caption Item 1', + }, + { + 'name': 'item 2', + 'value': 'value 2', + 'caption': 'Caption Item 2', + }, + ], + } + self. _InitWriterForAddingPolicies(self.writer, list_policy) + self.writer.WritePolicy(list_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'List policy caption\n' + '' + 'This is a test description.\n' + 'List policy caption') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' List policy label\n' + '') + self.AssertXMLEquals(output, expected_output) + + def testDictionaryPolicy(self): + dict_policy = { + 'name': 'DictionaryPolicyStub', + 'type': 'dict', + 'caption': 'Dictionary policy caption', + 'label': 'Dictionary policy label', + 'desc': 'This is a test description.', + } + self. _InitWriterForAddingPolicies(self.writer, dict_policy) + self.writer.WritePolicy(dict_policy) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'Dictionary policy caption\n' + '' + 'This is a test description.') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testPlatform(self): + # Test that the writer correctly chooses policies of platform Windows. + self.assertTrue(self.writer.IsPolicySupported({ + 'supported_on': [ + {'platforms': ['win', 'zzz']}, {'platforms': ['aaa']} + ] + })) + self.assertFalse(self.writer.IsPolicySupported({ + 'supported_on': [ + {'platforms': ['mac', 'linux']}, {'platforms': ['aaa']} + ] + })) + + def testStringEncodings(self): + enum_policy_a = { + 'name': 'EnumPolicy.A', + 'type': 'string-enum', + 'caption': 'Enum policy A caption', + 'label': 'Enum policy A label', + 'desc': 'This is a test description.', + 'items': [ + { + 'name': 'tls1.2', + 'value': 'tls1.2', + 'caption': 'tls1.2', + } + ], + } + enum_policy_b = { + 'name': 'EnumPolicy.B', + 'type': 'string-enum', + 'caption': 'Enum policy B caption', + 'label': 'Enum policy B label', + 'desc': 'This is a test description.', + 'items': [ + { + 'name': 'tls1.2', + 'value': 'tls1.2', + 'caption': 'tls1.2', + } + ], + } + self. _InitWriterForAddingPolicies(self.writer, enum_policy_a) + self.writer.WritePolicy(enum_policy_a) + self.writer.WritePolicy(enum_policy_b) + # Assert generated string elements. + output = self.GetXMLOfChildren(self.writer._string_table_elem) + expected_output = ( + 'Enum policy A caption\n' + '' + 'This is a test description.\n' + 'tls1.2\n' + 'Enum policy B caption\n' + '' + 'This is a test description.\n') + self.AssertXMLEquals(output, expected_output) + # Assert generated presentation elements. + output = self.GetXMLOfChildren(self.writer._presentation_table_elem) + expected_output = ( + '\n' + ' ' + 'Enum policy A label\n' + '\n' + '\n' + ' ' + 'Enum policy B label\n' + '') + self.AssertXMLEquals(output, expected_output) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/admx_writer.py b/tools/grit/grit/format/policy_templates/writers/admx_writer.py new file mode 100755 index 0000000..d657834 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/admx_writer.py @@ -0,0 +1,382 @@ +#!/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. + +from xml.dom import minidom +from grit.format.policy_templates.writers import xml_formatted_writer + + +def GetWriter(config): + '''Factory method for instanciating the ADMXWriter. Every Writer needs a + GetWriter method because the TemplateFormatter uses this method to + instantiate a Writer. + ''' + return ADMXWriter(['win'], config) + + +class ADMXWriter(xml_formatted_writer.XMLFormattedWriter): + '''Class for generating an ADMX policy template. It is used by the + PolicyTemplateGenerator to write the admx file. + ''' + + # DOM root node of the generated ADMX document. + _doc = None + + # The ADMX "policies" element that contains the ADMX "policy" elements that + # are generated. + _active_policies_elem = None + + def _AdmlString(self, name): + '''Creates a reference to the named string in an ADML file. + Args: + name: Name of the referenced ADML string. + ''' + name = name.replace('.', '_') + return '$(string.' + name + ')' + + def _AdmlStringExplain(self, name): + '''Creates a reference to the named explanation string in an ADML file. + Args: + name: Name of the referenced ADML explanation. + ''' + name = name.replace('.', '_') + return '$(string.' + name + '_Explain)' + + def _AdmlPresentation(self, name): + '''Creates a reference to the named presentation element in an ADML file. + Args: + name: Name of the referenced ADML presentation element. + ''' + return '$(presentation.' + name + ')' + + def _AddPolicyNamespaces(self, parent, prefix, namespace): + '''Generates the ADMX "policyNamespace" element and adds the elements to the + passed parent element. The namespace of the generated ADMX document is + define via the ADMX "target" element. Used namespaces are declared with an + ADMX "using" element. ADMX "target" and "using" elements are children of the + ADMX "policyNamespace" element. + + Args: + parent: The parent node to which all generated elements are added. + prefix: A logical name that can be used in the generated ADMX document to + refere to this namespace. + namespace: Namespace of the generated ADMX document. + ''' + policy_namespaces_elem = self.AddElement(parent, 'policyNamespaces') + attributes = { + 'prefix': prefix, + 'namespace': namespace, + } + self.AddElement(policy_namespaces_elem, 'target', attributes) + attributes = { + 'prefix': 'windows', + 'namespace': 'Microsoft.Policies.Windows', + } + self.AddElement(policy_namespaces_elem, 'using', attributes) + + def _AddCategory(self, parent, name, display_name, + parent_category_name=None): + '''Adds an ADMX category element to the passed parent node. The following + snippet shows an example of a category element where "chromium" is the value + of the parameter name: + + + + Each parent node can have only one category with a given name. Adding the + same category again with the same attributes is ignored, but adding it + again with different attributes is an error. + + Args: + parent: The parent node to which all generated elements are added. + name: Name of the category. + display_name: Display name of the category. + parent_category_name: Name of the parent category. Defaults to None. + ''' + existing = filter(lambda e: e.getAttribute('name') == name, + parent.getElementsByTagName('category')) + if existing: + assert len(existing) == 1 + assert existing[0].getAttribute('name') == name + assert existing[0].getAttribute('displayName') == display_name + return + attributes = { + 'name': name, + 'displayName': display_name, + } + category_elem = self.AddElement(parent, 'category', attributes) + if parent_category_name: + attributes = {'ref': parent_category_name} + self.AddElement(category_elem, 'parentCategory', attributes) + + def _AddCategories(self, categories): + '''Generates the ADMX "categories" element and adds it to the categories + main node. The "categories" element defines the category for the policies + defined in this ADMX document. Here is an example of an ADMX "categories" + element: + + + + + + + + + Args: + categories_path: The categories path e.g. ['google', 'googlechrome']. For + each level in the path a "category" element will be generated. Except + for the root level, each level refers to its parent. Since the root + level category has no parent it does not require a parent reference. + ''' + category_name = None + for category in categories: + parent_category_name = category_name + category_name = category + self._AddCategory(self._categories_elem, category_name, + self._AdmlString(category_name), parent_category_name) + + def _AddSupportedOn(self, parent, supported_os): + '''Generates the "supportedOn" ADMX element and adds it to the passed + parent node. The "supportedOn" element contains information about supported + Windows OS versions. The following code snippet contains an example of a + "supportedOn" element: + + + + + + ... + + + Args: + parent: The parent element to which all generated elements are added. + supported_os: List with all supported Win OSes. + ''' + supported_on_elem = self.AddElement(parent, 'supportedOn') + definitions_elem = self.AddElement(supported_on_elem, 'definitions') + attributes = { + 'name': supported_os, + 'displayName': self._AdmlString(supported_os) + } + self.AddElement(definitions_elem, 'definition', attributes) + + def _AddStringPolicy(self, parent, name): + '''Generates ADMX elements for a String-Policy and adds them to the + passed parent node. + ''' + attributes = { + 'id': name, + 'valueName': name, + 'maxLength': '1000000', + } + self.AddElement(parent, 'text', attributes) + + def _AddIntPolicy(self, parent, name): + '''Generates ADMX elements for an Int-Policy and adds them to the passed + parent node. + ''' + attributes = { + 'id': name, + 'valueName': name, + 'maxValue': '2000000000', + } + self.AddElement(parent, 'decimal', attributes) + + def _AddEnumPolicy(self, parent, policy): + '''Generates ADMX elements for an Enum-Policy and adds them to the + passed parent element. + ''' + name = policy['name'] + items = policy['items'] + attributes = { + 'id': name, + 'valueName': name, + } + enum_elem = self.AddElement(parent, 'enum', attributes) + for item in items: + attributes = {'displayName': self._AdmlString(item['name'])} + item_elem = self.AddElement(enum_elem, 'item', attributes) + value_elem = self.AddElement(item_elem, 'value') + value_string = str(item['value']) + if policy['type'] == 'int-enum': + self.AddElement(value_elem, 'decimal', {'value': value_string}) + else: + self.AddElement(value_elem, 'string', {}, value_string) + + def _AddListPolicy(self, parent, key, name): + '''Generates ADMX XML elements for a List-Policy and adds them to the + passed parent element. + ''' + attributes = { + # The ID must be in sync with ID of the corresponding element in the ADML + # file. + 'id': name + 'Desc', + 'valuePrefix': '', + 'key': key + '\\' + name, + } + self.AddElement(parent, 'list', attributes) + + def _AddMainPolicy(self, parent): + '''Generates ADMX elements for a Main-Policy amd adds them to the + passed parent element. + ''' + enabled_value_elem = self.AddElement(parent, 'enabledValue'); + self.AddElement(enabled_value_elem, 'decimal', {'value': '1'}) + disabled_value_elem = self.AddElement(parent, 'disabledValue'); + self.AddElement(disabled_value_elem, 'decimal', {'value': '0'}) + + def _GetElements(self, policy_group_elem): + '''Returns the ADMX "elements" child from an ADMX "policy" element. If the + "policy" element has no "elements" child yet, a new child is created. + + Args: + policy_group_elem: The ADMX "policy" element from which the child element + "elements" is returned. + + Raises: + Exception: The policy_group_elem does not contain a ADMX "policy" element. + ''' + if policy_group_elem.tagName != 'policy': + raise Exception('Expected a "policy" element but got a "%s" element' + % policy_group_elem.tagName) + elements_list = policy_group_elem.getElementsByTagName('elements'); + if len(elements_list) == 0: + return self.AddElement(policy_group_elem, 'elements') + elif len(elements_list) == 1: + return elements_list[0] + else: + raise Exception('There is supposed to be only one "elements" node but' + ' there are %s.' % str(len(elements_list))) + + def _WritePolicy(self, policy, name, key, parent): + '''Generates AMDX elements for a Policy. There are four different policy + types: Main-Policy, String-Policy, Enum-Policy and List-Policy. + ''' + policies_elem = self._active_policies_elem + policy_type = policy['type'] + policy_name = policy['name'] + if policy_type == 'external': + # This type can only be set through cloud policy. + return + + attributes = { + 'name': name, + 'class': self.config['win_group_policy_class'], + 'displayName': self._AdmlString(policy_name), + 'explainText': self._AdmlStringExplain(policy_name), + 'presentation': self._AdmlPresentation(policy_name), + 'key': key, + } + # Store the current "policy" AMDX element in self for later use by the + # WritePolicy method. + policy_elem = self.AddElement(policies_elem, 'policy', + attributes) + self.AddElement(policy_elem, 'parentCategory', + {'ref': parent}) + self.AddElement(policy_elem, 'supportedOn', + {'ref': self.config['win_supported_os']}) + if policy_type == 'main': + self.AddAttribute(policy_elem, 'valueName', policy_name) + self._AddMainPolicy(policy_elem) + elif policy_type in ('string', 'dict'): + # 'dict' policies are configured as JSON-encoded strings on Windows. + parent = self._GetElements(policy_elem) + self._AddStringPolicy(parent, policy_name) + elif policy_type == 'int': + parent = self._GetElements(policy_elem) + self._AddIntPolicy(parent, policy_name) + elif policy_type in ('int-enum', 'string-enum'): + parent = self._GetElements(policy_elem) + self._AddEnumPolicy(parent, policy) + elif policy_type in ('list', 'string-enum-list'): + parent = self._GetElements(policy_elem) + self._AddListPolicy(parent, key, policy_name) + elif policy_type == 'group': + pass + else: + raise Exception('Unknown policy type %s.' % policy_type) + + def WritePolicy(self, policy): + if self.CanBeMandatory(policy): + self._WritePolicy(policy, + policy['name'], + self.config['win_reg_mandatory_key_name'], + self._active_mandatory_policy_group_name) + + def WriteRecommendedPolicy(self, policy): + self._WritePolicy(policy, + policy['name'] + '_recommended', + self.config['win_reg_recommended_key_name'], + self._active_recommended_policy_group_name) + + def _BeginPolicyGroup(self, group, name, parent): + '''Generates ADMX elements for a Policy-Group. + ''' + attributes = { + 'name': name, + 'displayName': self._AdmlString(group['name'] + '_group'), + } + category_elem = self.AddElement(self._categories_elem, + 'category', + attributes) + attributes = { + 'ref': parent + } + self.AddElement(category_elem, 'parentCategory', attributes) + + def BeginPolicyGroup(self, group): + self._BeginPolicyGroup(group, + group['name'], + self.config['win_mandatory_category_path'][-1]) + self._active_mandatory_policy_group_name = group['name'] + + def EndPolicyGroup(self): + self._active_mandatory_policy_group_name = \ + self.config['win_mandatory_category_path'][-1] + + def BeginRecommendedPolicyGroup(self, group): + self._BeginPolicyGroup(group, + group['name'] + '_recommended', + self.config['win_recommended_category_path'][-1]) + self._active_recommended_policy_group_name = group['name'] + '_recommended' + + def EndRecommendedPolicyGroup(self): + self._active_recommended_policy_group_name = \ + self.config['win_recommended_category_path'][-1] + + def BeginTemplate(self): + '''Generates the skeleton of the ADMX template. An ADMX template contains + an ADMX "PolicyDefinitions" element with four child nodes: "policies" + "policyNamspaces", "resources", "supportedOn" and "categories" + ''' + dom_impl = minidom.getDOMImplementation('') + self._doc = dom_impl.createDocument(None, 'policyDefinitions', None) + if self._GetChromiumVersionString() is not None: + self.AddComment(self._doc.documentElement, self.config['build'] + \ + ' version: ' + self._GetChromiumVersionString()) + policy_definitions_elem = self._doc.documentElement + + policy_definitions_elem.attributes['revision'] = '1.0' + policy_definitions_elem.attributes['schemaVersion'] = '1.0' + + self._AddPolicyNamespaces(policy_definitions_elem, + self.config['admx_prefix'], + self.config['admx_namespace']) + self.AddElement(policy_definitions_elem, 'resources', + {'minRequiredRevision' : '1.0'}) + self._AddSupportedOn(policy_definitions_elem, + self.config['win_supported_os']) + self._categories_elem = self.AddElement(policy_definitions_elem, + 'categories') + self._AddCategories(self.config['win_mandatory_category_path']) + self._AddCategories(self.config['win_recommended_category_path']) + self._active_policies_elem = self.AddElement(policy_definitions_elem, + 'policies') + self._active_mandatory_policy_group_name = \ + self.config['win_mandatory_category_path'][-1] + self._active_recommended_policy_group_name = \ + self.config['win_recommended_category_path'][-1] + + def GetTemplateText(self): + return self.ToPrettyXml(self._doc) diff --git a/tools/grit/grit/format/policy_templates/writers/admx_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/admx_writer_unittest.py new file mode 100755 index 0000000..8bdb7a4 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/admx_writer_unittest.py @@ -0,0 +1,573 @@ +#!/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. + + +"""Unittests for grit.format.policy_templates.writers.admx_writer.""" + + +import os +import sys +import unittest +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + + +from grit.format.policy_templates.writers import admx_writer +from grit.format.policy_templates.writers import xml_writer_base_unittest +from xml.dom import minidom + + +class AdmxWriterUnittest(xml_writer_base_unittest.XmlWriterBaseTest): + + def _CreateDocumentElement(self): + dom_impl = minidom.getDOMImplementation('') + doc = dom_impl.createDocument(None, 'root', None) + return doc.documentElement + + def setUp(self): + # Writer configuration. This dictionary contains parameter used by the ADMX + # Writer + config = { + 'win_group_policy_class': 'TestClass', + 'win_supported_os': 'SUPPORTED_TESTOS', + 'win_reg_mandatory_key_name': 'Software\\Policies\\Test', + 'win_reg_recommended_key_name': 'Software\\Policies\\Test\\Recommended', + 'win_mandatory_category_path': ['test_category'], + 'win_recommended_category_path': ['test_recommended_category'], + 'admx_namespace': 'ADMXWriter.Test.Namespace', + 'admx_prefix': 'test_prefix', + 'build': 'test_product', + } + self.writer = admx_writer.GetWriter(config) + self.writer.Init() + + def _GetPoliciesElement(self, doc): + node_list = doc.getElementsByTagName('policies') + self.assertTrue(node_list.length == 1) + return node_list.item(0) + + def _GetCategoriesElement(self, doc): + node_list = doc.getElementsByTagName('categories') + self.assertTrue(node_list.length == 1) + return node_list.item(0) + + def testEmpty(self): + self.writer.BeginTemplate() + self.writer.EndTemplate() + + output = self.writer.GetTemplateText() + expected_output = ( + '\n' + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testEmptyVersion(self): + self.writer.config['version'] = '39.0.0.0' + self.writer.BeginTemplate() + self.writer.EndTemplate() + + output = self.writer.GetTemplateText() + expected_output = ( + '\n' + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testEmptyPolicyGroup(self): + empty_policy_group = { + 'name': 'PolicyGroup', + 'policies': [] + } + # Initialize writer to write a policy group. + self.writer.BeginTemplate() + # Write policy group + self.writer.BeginPolicyGroup(empty_policy_group) + self.writer.EndPolicyGroup() + + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = '' + self.AssertXMLEquals(output, expected_output) + + output = self.GetXMLOfChildren( + self._GetCategoriesElement(self.writer._doc)) + expected_output = ( + '\n' + '\n' + '\n' + ' \n' + '') + + self.AssertXMLEquals(output, expected_output) + + def testPolicyGroup(self): + empty_policy_group = { + 'name': 'PolicyGroup', + 'policies': [ + {'name': 'PolicyStub2', + 'type': 'main'}, + {'name': 'PolicyStub1', + 'type': 'main'}, + ] + } + # Initialize writer to write a policy group. + self.writer.BeginTemplate() + # Write policy group + self.writer.BeginPolicyGroup(empty_policy_group) + self.writer.EndPolicyGroup() + + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = '' + self.AssertXMLEquals(output, expected_output) + + output = self.GetXMLOfChildren( + self._GetCategoriesElement(self.writer._doc)) + expected_output = ( + '\n' + '\n' + '\n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + + def _initWriterForPolicy(self, writer, policy): + '''Initializes the writer to write the given policy next. + ''' + policy_group = { + 'name': 'PolicyGroup', + 'policies': [policy] + } + writer.BeginTemplate() + writer.BeginPolicyGroup(policy_group) + + def testMainPolicy(self): + main_policy = { + 'name': 'DummyMainPolicy', + 'type': 'main', + } + + self._initWriterForPolicy(self.writer, main_policy) + + self.writer.WritePolicy(main_policy) + + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + + self.AssertXMLEquals(output, expected_output) + + def testRecommendedPolicy(self): + main_policy = { + 'name': 'DummyMainPolicy', + 'type': 'main', + } + + policy_group = { + 'name': 'PolicyGroup', + 'policies': [main_policy], + } + self.writer.BeginTemplate() + self.writer.BeginRecommendedPolicyGroup(policy_group) + + self.writer.WriteRecommendedPolicy(main_policy) + + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + + self.AssertXMLEquals(output, expected_output) + + def testRecommendedOnlyPolicy(self): + main_policy = { + 'name': 'DummyMainPolicy', + 'type': 'main', + 'features': { + 'can_be_recommended': True, + 'can_be_mandatory': False, + } + } + + policy_group = { + 'name': 'PolicyGroup', + 'policies': [main_policy], + } + self.writer.BeginTemplate() + self.writer.BeginRecommendedPolicyGroup(policy_group) + + self.writer.WritePolicy(main_policy) + self.writer.WriteRecommendedPolicy(main_policy) + + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + + self.AssertXMLEquals(output, expected_output) + + def testStringPolicy(self): + string_policy = { + 'name': 'SampleStringPolicy', + 'type': 'string', + } + self._initWriterForPolicy(self.writer, string_policy) + + self.writer.WritePolicy(string_policy) + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testIntPolicy(self): + int_policy = { + 'name': 'SampleIntPolicy', + 'type': 'int', + } + self._initWriterForPolicy(self.writer, int_policy) + + self.writer.WritePolicy(int_policy) + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testIntEnumPolicy(self): + enum_policy = { + 'name': 'SampleEnumPolicy', + 'type': 'int-enum', + 'items': [ + {'name': 'item_1', 'value': 0}, + {'name': 'item_2', 'value': 1}, + ] + } + + self._initWriterForPolicy(self.writer, enum_policy) + self.writer.WritePolicy(enum_policy) + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testStringEnumPolicy(self): + enum_policy = { + 'name': 'SampleEnumPolicy', + 'type': 'string-enum', + 'items': [ + {'name': 'item_1', 'value': 'one'}, + {'name': 'item_2', 'value': 'two'}, + ] + } + + # This test is different than the others because it also tests that space + # usage inside nodes is correct. + dom_impl = minidom.getDOMImplementation('') + self.writer._doc = dom_impl.createDocument(None, 'policyDefinitions', None) + self.writer._active_policies_elem = self.writer._doc.documentElement + self.writer._active_mandatory_policy_group_name = 'PolicyGroup' + self.writer.WritePolicy(enum_policy) + output = self.writer.GetTemplateText() + expected_output = ( + '\n' + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' one\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' two\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testListPolicy(self): + list_policy = { + 'name': 'SampleListPolicy', + 'type': 'list', + } + self._initWriterForPolicy(self.writer, list_policy) + self.writer.WritePolicy(list_policy) + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + + self.AssertXMLEquals(output, expected_output) + + def testStringEnumListPolicy(self): + list_policy = { + 'name': 'SampleListPolicy', + 'type': 'string-enum-list', + 'items': [ + {'name': 'item_1', 'value': 'one'}, + {'name': 'item_2', 'value': 'two'}, + ] + } + self._initWriterForPolicy(self.writer, list_policy) + self.writer.WritePolicy(list_policy) + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + + self.AssertXMLEquals(output, expected_output) + + def testDictionaryPolicy(self): + dict_policy = { + 'name': 'SampleDictionaryPolicy', + 'type': 'dict', + } + self._initWriterForPolicy(self.writer, dict_policy) + + self.writer.WritePolicy(dict_policy) + output = self.GetXMLOfChildren(self._GetPoliciesElement(self.writer._doc)) + expected_output = ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + def testPlatform(self): + # Test that the writer correctly chooses policies of platform Windows. + self.assertTrue(self.writer.IsPolicySupported({ + 'supported_on': [ + {'platforms': ['win', 'zzz']}, {'platforms': ['aaa']} + ] + })) + self.assertFalse(self.writer.IsPolicySupported({ + 'supported_on': [ + {'platforms': ['mac', 'linux']}, {'platforms': ['aaa']} + ] + })) + + def testStringEncodings(self): + enum_policy_a = { + 'name': 'SampleEnumPolicy.A', + 'type': 'string-enum', + 'items': [ + {'name': 'tls1.2', 'value': 'tls1.2'} + ] + } + enum_policy_b = { + 'name': 'SampleEnumPolicy.B', + 'type': 'string-enum', + 'items': [ + {'name': 'tls1.2', 'value': 'tls1.2'} + ] + } + + dom_impl = minidom.getDOMImplementation('') + self.writer._doc = dom_impl.createDocument(None, 'policyDefinitions', None) + self.writer._active_policies_elem = self.writer._doc.documentElement + self.writer._active_mandatory_policy_group_name = 'PolicyGroup' + self.writer.WritePolicy(enum_policy_a) + self.writer.WritePolicy(enum_policy_b) + output = self.writer.GetTemplateText() + expected_output = ( + '\n' + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' tls1.2\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' tls1.2\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '') + self.AssertXMLEquals(output, expected_output) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/android_policy_writer.py b/tools/grit/grit/format/policy_templates/writers/android_policy_writer.py new file mode 100755 index 0000000..5d8cbef --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/android_policy_writer.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python +# Copyright (c) 2015 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 grit.format.policy_templates.writers import xml_formatted_writer +from xml.dom import minidom +from xml.sax import saxutils as xml_escape + + +def GetWriter(config): + '''Factory method for creating AndroidPolicyWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return AndroidPolicyWriter(['android'], config) + + +def _EscapeResource(resource): + '''Escape the resource for usage in an Android resource XML file. + This includes standard XML escaping as well as those specific to Android. + ''' + if type(resource) == int: + return str(resource) + return xml_escape.escape(resource, {"'": "\\'", '"': '\\"', '\\': '\\\\'}) + + +class AndroidPolicyWriter(xml_formatted_writer.XMLFormattedWriter): + '''Outputs localized Android Resource XML files. + The policy strings are localized and exposed as string resources for + consumption through Android's App restriction Schema. + ''' + + # DOM root node of the generated XML document. + _doc = None + # The resources node contains all resource 'string' and 'string-array' + # elements. + _resources = None + + def AddStringResource(self, name, string): + '''Add a string resource of the given name. + ''' + string_node = self._doc.createElement('string') + string_node.setAttribute('name', name) + string_node.appendChild(self._doc.createTextNode(_EscapeResource(string))) + self._resources.appendChild(string_node) + + def AddStringArrayResource(self, name, string_items): + '''Add a string-array resource of the given name and + elements from string_items. + ''' + string_array_node = self._doc.createElement('string-array') + string_array_node.setAttribute('name', name) + self._resources.appendChild(string_array_node) + for item in string_items: + string_node = self._doc.createElement('item') + string_node.appendChild(self._doc.createTextNode(_EscapeResource(item))) + string_array_node.appendChild(string_node) + + def PreprocessPolicies(self, policy_list): + return self.FlattenGroupsAndSortPolicies(policy_list) + + def CanBeRecommended(self, policy): + return False + + def IsDeprecatedPolicySupported(self, policy): + return True + + def IsFuturePolicySupported(self, policy): + return True + + def WritePolicy(self, policy): + name = policy['name'] + self.AddStringResource(name + 'Title', policy['caption']) + + # Get the first line of the policy description. + description = policy['desc'].split('\n', 1)[0] + self.AddStringResource(name + 'Desc', description) + + items = policy.get('items') + if items is not None: + entries = [ item['caption'] for item in items ] + values = [ item['value'] for item in items ] + self.AddStringArrayResource(name + 'Entries', entries) + self.AddStringArrayResource(name + 'Values', values) + + def BeginTemplate(self): + comment_text = 'DO NOT MODIFY THIS FILE DIRECTLY!\n' \ + 'IT IS GENERATED FROM policy_templates.json.' + comment_node = self._doc.createComment(comment_text) + self._doc.insertBefore(comment_node, self._resources) + + def Init(self): + impl = minidom.getDOMImplementation() + self._doc = impl.createDocument(None, 'resources', None) + self._resources = self._doc.documentElement + + def GetTemplateText(self): + return self.ToPrettyXml(self._doc) diff --git a/tools/grit/grit/format/policy_templates/writers/android_policy_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/android_policy_writer_unittest.py new file mode 100755 index 0000000..437c71a --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/android_policy_writer_unittest.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +# Copyright (c) 2015 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.policy_templates.writers.android_policy_writer''' + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest +from xml.dom import minidom + +from grit.format.policy_templates.writers import writer_unittest_common +from grit.format.policy_templates.writers import android_policy_writer + + +class AndroidPolicyWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests to test assumptions in Android Policy Writer''' + + def testPolicyWithoutItems(self): + # Test an example policy without items. + policy = { + 'name': '_policy_name', + 'caption': '_policy_caption', + 'desc': 'This is a long policy caption. More than one sentence ' + 'in a single line because it is very important.\nIgnore this.' + } + writer = android_policy_writer.GetWriter({}) + writer.Init() + writer.BeginTemplate() + writer.WritePolicy(policy) + self.assertEquals( + writer._resources.toxml(), + '' + '_policy_caption' + 'This is a long policy caption. More ' + 'than one sentence in a single line because it is very important.' + '' + '') + + def testPolicyWithItems(self): + # Test an example policy without items. + policy = { + 'name': '_policy_name', + 'caption': '_policy_caption', + 'desc': '_policy_desc_first.\n_ignored_policy_desc', + 'items': [ + { + 'caption':'_caption1', + 'value':'_value1', + }, + { + 'caption':'_caption2', + 'value':'_value2', + } + ] + } + writer = android_policy_writer.GetWriter({}) + writer.Init() + writer.BeginTemplate() + writer.WritePolicy(policy) + self.assertEquals( + writer._resources.toxml(), + '' + '_policy_caption' + '_policy_desc_first.' + '' + '_caption1' + '_caption2' + '' + '' + '_value1' + '_value2' + '' + '') + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/doc_writer.py b/tools/grit/grit/format/policy_templates/writers/doc_writer.py new file mode 100755 index 0000000..ed6d060 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/doc_writer.py @@ -0,0 +1,748 @@ +#!/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. + + +import json +from xml.dom import minidom +from grit import lazy_re +from grit.format.policy_templates.writers import xml_formatted_writer + + +def GetWriter(config): + '''Factory method for creating DocWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return DocWriter(['*'], config) + + +class DocWriter(xml_formatted_writer.XMLFormattedWriter): + '''Class for generating policy templates in HTML format. + The intended use of the generated file is to upload it on + http://dev.chromium.org, therefore its format has some limitations: + - No HTML and body tags. + - Restricted set of element attributes: for example no 'class'. + Because of the latter the output is styled using the 'style' + attributes of HTML elements. This is supported by the dictionary + self._STYLES[] and the method self._AddStyledElement(), they try + to mimic the functionality of CSS classes. (But without inheritance.) + + This class is invoked by PolicyTemplateGenerator to create the HTML + files. + ''' + + def _GetLocalizedMessage(self, msg_id): + '''Returns a localized message for this writer. + + Args: + msg_id: The identifier of the message. + + Returns: + The localized message. + ''' + return self.messages['doc_' + msg_id]['text'] + + def _MapListToString(self, item_map, items): + '''Creates a comma-separated list. + + Args: + item_map: A dictionary containing all the elements of 'items' as + keys. + items: A list of arbitrary items. + + Returns: + Looks up each item of 'items' in 'item_maps' and concatenates the + resulting items into a comma-separated list. + ''' + return ', '.join([item_map[x] for x in items]) + + def _AddTextWithLinks(self, parent, text): + '''Parse a string for URLs and add it to a DOM node with the URLs replaced + with HTML links. + + Args: + parent: The DOM node to which the text will be added. + text: The string to be added. + ''' + # A simple regexp to search for URLs. It is enough for now. + url_matcher = lazy_re.compile('(http://[^\\s]*[^\\s\\.])') + + # Iterate through all the URLs and replace them with links. + while True: + # Look for the first URL. + res = url_matcher.search(text) + if not res: + break + # Calculate positions of the substring of the URL. + url = res.group(0) + start = res.start(0) + end = res.end(0) + # Add the text prior to the URL. + self.AddText(parent, text[:start]) + # Add a link for the URL. + self.AddElement(parent, 'a', {'href': url}, url) + # Drop the part of text that is added. + text = text[end:] + self.AddText(parent, text) + + def _AddParagraphs(self, parent, text): + '''Break description into paragraphs and replace URLs with links. + + Args: + parent: The DOM node to which the text will be added. + text: The string to be added. + ''' + # Split text into list of paragraphs. + entries = text.split('\n\n') + for entry in entries: + # Create a new paragraph node. + paragraph = self.AddElement(parent, 'p') + # Insert text to the paragraph with processing the URLs. + self._AddTextWithLinks(paragraph, entry) + + def _AddStyledElement(self, parent, name, style_ids, attrs=None, text=None): + '''Adds an XML element to a parent, with CSS style-sheets included. + + Args: + parent: The parent DOM node. + name: Name of the element to add. + style_ids: A list of CSS style strings from self._STYLE[]. + attrs: Dictionary of attributes for the element. + text: Text content for the element. + ''' + if attrs == None: + attrs = {} + + style = ''.join([self._STYLE[x] for x in style_ids]) + if style != '': + # Apply the style specified by style_ids. + attrs['style'] = style + attrs.get('style', '') + return self.AddElement(parent, name, attrs, text) + + def _AddDescription(self, parent, policy): + '''Adds a string containing the description of the policy. URLs are + replaced with links and the possible choices are enumerated in case + of 'string-enum' and 'int-enum' type policies. + + Args: + parent: The DOM node for which the feature list will be added. + policy: The data structure of a policy. + ''' + # Add description by paragraphs (URLs will be substituted by links). + self._AddParagraphs(parent, policy['desc']) + # Add list of enum items. + if policy['type'] in ('string-enum', 'int-enum', 'string-enum-list'): + ul = self.AddElement(parent, 'ul') + for item in policy['items']: + if policy['type'] == 'int-enum': + value_string = str(item['value']) + else: + value_string = '"%s"' % item['value'] + self.AddElement( + ul, 'li', {}, '%s = %s' % (value_string, item['caption'])) + + def _AddFeatures(self, parent, policy): + '''Adds a string containing the list of supported features of a policy + to a DOM node. The text will look like as: + Feature_X: Yes, Feature_Y: No + + Args: + parent: The DOM node for which the feature list will be added. + policy: The data structure of a policy. + ''' + features = [] + # The sorting is to make the order well-defined for testing. + keys = policy['features'].keys() + keys.sort() + for key in keys: + key_name = self._FEATURE_MAP[key] + if policy['features'][key]: + value_name = self._GetLocalizedMessage('supported') + else: + value_name = self._GetLocalizedMessage('not_supported') + features.append('%s: %s' % (key_name, value_name)) + self.AddText(parent, ', '.join(features)) + + def _AddListExampleMac(self, parent, policy): + '''Adds an example value for Mac of a 'list' policy to a DOM node. + + Args: + parent: The DOM node for which the example will be added. + policy: A policy of type 'list', for which the Mac example value + is generated. + ''' + example_value = policy['example_value'] + self.AddElement(parent, 'dt', {}, 'Mac:') + mac = self._AddStyledElement(parent, 'dd', ['.monospace', '.pre']) + + mac_text = [''] + for item in example_value: + mac_text.append(' %s' % item) + mac_text.append('') + self.AddText(mac, '\n'.join(mac_text)) + + def _AddListExampleWindows(self, parent, policy): + '''Adds an example value for Windows of a 'list' policy to a DOM node. + + Args: + parent: The DOM node for which the example will be added. + policy: A policy of type 'list', for which the Windows example value + is generated. + ''' + example_value = policy['example_value'] + self.AddElement(parent, 'dt', {}, 'Windows:') + win = self._AddStyledElement(parent, 'dd', ['.monospace', '.pre']) + win_text = [] + cnt = 1 + if self.CanBeRecommended(policy) and not self.CanBeMandatory(policy): + key_name = self.config['win_reg_recommended_key_name'] + else: + key_name = self.config['win_reg_mandatory_key_name'] + for item in example_value: + win_text.append( + '%s\\%s\\%d = "%s"' % + (key_name, policy['name'], cnt, item)) + cnt = cnt + 1 + self.AddText(win, '\n'.join(win_text)) + + def _AddListExampleAndroidLinux(self, parent, policy): + '''Adds an example value for Android/Linux of a 'list' policy to a DOM node. + + Args: + parent: The DOM node for which the example will be added. + policy: A policy of type 'list', for which the Android/Linux example value + is generated. + ''' + example_value = policy['example_value'] + self.AddElement(parent, 'dt', {}, 'Android/Linux:') + element = self._AddStyledElement(parent, 'dd', ['.monospace']) + text = [] + for item in example_value: + text.append('"%s"' % item) + self.AddText(element, '[%s]' % ', '.join(text)) + + def _AddListExample(self, parent, policy): + '''Adds the example value of a 'list' policy to a DOM node. Example output: +
+
Windows:
+
+ Software\Policies\Chromium\DisabledPlugins\0 = "Java" + Software\Policies\Chromium\DisabledPlugins\1 = "Shockwave Flash" +
+
Android/Linux:
+
["Java", "Shockwave Flash"]
+
Mac:
+
+ + Java + Shockwave Flash + +
+
+ + Args: + parent: The DOM node for which the example will be added. + policy: The data structure of a policy. + ''' + examples = self._AddStyledElement(parent, 'dl', ['dd dl']) + if self.IsPolicySupportedOnPlatform(policy, 'win'): + self._AddListExampleWindows(examples, policy) + if (self.IsPolicySupportedOnPlatform(policy, 'android') or + self.IsPolicySupportedOnPlatform(policy, 'linux')): + self._AddListExampleAndroidLinux(examples, policy) + if self.IsPolicySupportedOnPlatform(policy, 'mac'): + self._AddListExampleMac(examples, policy) + + def _PythonObjectToPlist(self, obj, indent=''): + '''Converts a python object to an equivalent XML plist. + + Returns a list of lines.''' + obj_type = type(obj) + if obj_type == bool: + return [ '%s<%s/>' % (indent, 'true' if obj else 'false') ] + elif obj_type == int: + return [ '%s%s' % (indent, obj) ] + elif obj_type == str: + return [ '%s%s' % (indent, obj) ] + elif obj_type == list: + result = [ '%s' % indent ] + for item in obj: + result += self._PythonObjectToPlist(item, indent + ' ') + result.append('%s' % indent) + return result + elif obj_type == dict: + result = [ '%s' % indent ] + for key in sorted(obj.keys()): + result.append('%s%s' % (indent + ' ', key)) + result += self._PythonObjectToPlist(obj[key], indent + ' ') + result.append('%s' % indent) + return result + else: + raise Exception('Invalid object to convert: %s' % obj) + + def _AddDictionaryExampleMac(self, parent, policy): + '''Adds an example value for Mac of a 'dict' policy to a DOM node. + + Args: + parent: The DOM node for which the example will be added. + policy: A policy of type 'dict', for which the Mac example value + is generated. + ''' + example_value = policy['example_value'] + self.AddElement(parent, 'dt', {}, 'Mac:') + mac = self._AddStyledElement(parent, 'dd', ['.monospace', '.pre']) + mac_text = ['%s' % (policy['name'])] + mac_text += self._PythonObjectToPlist(example_value) + self.AddText(mac, '\n'.join(mac_text)) + + def _AddDictionaryExampleWindows(self, parent, policy): + '''Adds an example value for Windows of a 'dict' policy to a DOM node. + + Args: + parent: The DOM node for which the example will be added. + policy: A policy of type 'dict', for which the Windows example value + is generated. + ''' + self.AddElement(parent, 'dt', {}, 'Windows:') + win = self._AddStyledElement(parent, 'dd', ['.monospace', '.pre']) + if self.CanBeRecommended(policy) and not self.CanBeMandatory(policy): + key_name = self.config['win_reg_recommended_key_name'] + else: + key_name = self.config['win_reg_mandatory_key_name'] + example = json.dumps(policy['example_value']) + self.AddText(win, '%s\\%s = %s' % (key_name, policy['name'], example)) + + def _AddDictionaryExampleAndroidLinux(self, parent, policy): + '''Adds an example value for Android/Linux of a 'dict' policy to a DOM node. + + Args: + parent: The DOM node for which the example will be added. + policy: A policy of type 'dict', for which the Android/Linux example value + is generated. + ''' + self.AddElement(parent, 'dt', {}, 'Android/Linux:') + element = self._AddStyledElement(parent, 'dd', ['.monospace']) + example = json.dumps(policy['example_value']) + self.AddText(element, '%s: %s' % (policy['name'], example)) + + def _AddDictionaryExample(self, parent, policy): + '''Adds the example value of a 'dict' policy to a DOM node. Example output: +
+
Windows:
+
+ Software\Policies\Chromium\ProxySettings = "{ 'ProxyMode': 'direct' }" +
+
Android/Linux:
+
"ProxySettings": { + "ProxyMode": "direct" + } +
+
Mac:
+
+ ProxySettings + + ProxyMode + direct + +
+
+ + Args: + parent: The DOM node for which the example will be added. + policy: The data structure of a policy. + ''' + examples = self._AddStyledElement(parent, 'dl', ['dd dl']) + if self.IsPolicySupportedOnPlatform(policy, 'win'): + self._AddDictionaryExampleWindows(examples, policy) + if (self.IsPolicySupportedOnPlatform(policy, 'android') or + self.IsPolicySupportedOnPlatform(policy, 'linux')): + self._AddDictionaryExampleAndroidLinux(examples, policy) + if self.IsPolicySupportedOnPlatform(policy, 'mac'): + self._AddDictionaryExampleMac(examples, policy) + + def _AddExample(self, parent, policy): + '''Adds the HTML DOM representation of the example value of a policy to + a DOM node. It is simple text for boolean policies, like + '0x00000001 (Windows), true (Linux), true (Android), (Mac)' + in case of boolean policies, but it may also contain other HTML elements. + (See method _AddListExample.) + + Args: + parent: The DOM node for which the example will be added. + policy: The data structure of a policy. + + Raises: + Exception: If the type of the policy is unknown or the example value + of the policy is out of its expected range. + ''' + example_value = policy['example_value'] + policy_type = policy['type'] + if policy_type == 'main': + pieces = [] + if self.IsPolicySupportedOnPlatform(policy, 'win'): + value = '0x00000001' if example_value else '0x00000000' + pieces.append(value + ' (Windows)') + if self.IsPolicySupportedOnPlatform(policy, 'linux'): + value = 'true' if example_value else 'false' + pieces.append(value + ' (Linux)') + if self.IsPolicySupportedOnPlatform(policy, 'android'): + value = 'true' if example_value else 'false' + pieces.append(value + ' (Android)') + if self.IsPolicySupportedOnPlatform(policy, 'mac'): + value = '' if example_value else '' + pieces.append(value + ' (Mac)') + self.AddText(parent, ', '.join(pieces)) + elif policy_type == 'string': + self.AddText(parent, '"%s"' % example_value) + elif policy_type in ('int', 'int-enum'): + pieces = [] + if self.IsPolicySupportedOnPlatform(policy, 'win'): + pieces.append('0x%08x (Windows)' % example_value) + if self.IsPolicySupportedOnPlatform(policy, 'linux'): + pieces.append('%d (Linux)' % example_value) + if self.IsPolicySupportedOnPlatform(policy, 'android'): + pieces.append('%d (Android)' % example_value) + if self.IsPolicySupportedOnPlatform(policy, 'mac'): + pieces.append('%d (Mac)' % example_value) + self.AddText(parent, ', '.join(pieces)) + elif policy_type == 'string-enum': + self.AddText(parent, '"%s"' % (example_value)) + elif policy_type in ('list', 'string-enum-list'): + self._AddListExample(parent, policy) + elif policy_type == 'dict': + self._AddDictionaryExample(parent, policy) + else: + raise Exception('Unknown policy type: ' + policy_type) + + def _AddPolicyAttribute(self, dl, term_id, + definition=None, definition_style=None): + '''Adds a term-definition pair to a HTML DOM
node. This method is + used by _AddPolicyDetails. Its result will have the form of: +
...
+
...
+ + Args: + dl: The DOM node of the
list. + term_id: A key to self._STRINGS[] which specifies the term of the pair. + definition: The text of the definition. (Optional.) + definition_style: List of references to values self._STYLE[] that specify + the CSS stylesheet of the
(definition) element. + + Returns: + The DOM node representing the definition
element. + ''' + # Avoid modifying the default value of definition_style. + if definition_style == None: + definition_style = [] + term = self._GetLocalizedMessage(term_id) + self._AddStyledElement(dl, 'dt', ['dt'], {}, term) + return self._AddStyledElement(dl, 'dd', definition_style, {}, definition) + + def _AddSupportedOnList(self, parent, supported_on_list): + '''Creates a HTML list containing the platforms, products and versions + that are specified in the list of supported_on. + + Args: + parent: The DOM node for which the list will be added. + supported_on_list: The list of supported products, as a list of + dictionaries. + ''' + ul = self._AddStyledElement(parent, 'ul', ['ul']) + for supported_on in supported_on_list: + text = [] + product = supported_on['product'] + platforms = supported_on['platforms'] + text.append(self._PRODUCT_MAP[product]) + text.append('(%s)' % + self._MapListToString(self._PLATFORM_MAP, platforms)) + if supported_on['since_version']: + since_version = self._GetLocalizedMessage('since_version') + text.append(since_version.replace('$6', supported_on['since_version'])) + if supported_on['until_version']: + until_version = self._GetLocalizedMessage('until_version') + text.append(until_version.replace('$6', supported_on['until_version'])) + # Add the list element: + self.AddElement(ul, 'li', {}, ' '.join(text)) + + def _AddPolicyDetails(self, parent, policy): + '''Adds the list of attributes of a policy to the HTML DOM node parent. + It will have the form: +
+
Attribute:
Description
+ ... +
+ + Args: + parent: A DOM element for which the list will be added. + policy: The data structure of the policy. + ''' + + dl = self.AddElement(parent, 'dl') + data_type = [self._TYPE_MAP[policy['type']]] + qualified_types = [] + is_complex_policy = False + if (self.IsPolicySupportedOnPlatform(policy, 'android') and + self._RESTRICTION_TYPE_MAP.get(policy['type'], None)): + qualified_types.append('Android:%s' % + self._RESTRICTION_TYPE_MAP[policy['type']]) + if policy['type'] in ('dict', 'list'): + is_complex_policy = True + if (self.IsPolicySupportedOnPlatform(policy, 'win') and + self._REG_TYPE_MAP.get(policy['type'], None)): + qualified_types.append('Windows:%s' % self._REG_TYPE_MAP[policy['type']]) + if policy['type'] == 'dict': + is_complex_policy = True + if qualified_types: + data_type.append('[%s]' % ', '.join(qualified_types)) + if is_complex_policy: + data_type.append('(%s)' % + self._GetLocalizedMessage('complex_policies_on_windows')) + self._AddPolicyAttribute(dl, 'data_type', ' '.join(data_type)) + if policy['type'] != 'external': + # All types except 'external' can be set through platform policy. + if self.IsPolicySupportedOnPlatform(policy, 'win'): + if self.CanBeRecommended(policy) and not self.CanBeMandatory(policy): + key_name = self.config['win_reg_recommended_key_name'] + else: + key_name = self.config['win_reg_mandatory_key_name'] + self._AddPolicyAttribute( + dl, + 'win_reg_loc', + key_name + '\\' + policy['name'], + ['.monospace']) + if (self.IsPolicySupportedOnPlatform(policy, 'linux') or + self.IsPolicySupportedOnPlatform(policy, 'mac')): + self._AddPolicyAttribute( + dl, + 'mac_linux_pref_name', + policy['name'], + ['.monospace']) + if self.IsPolicySupportedOnPlatform(policy, 'android'): + self._AddPolicyAttribute( + dl, + 'android_restriction_name', + policy['name'], + ['.monospace']) + dd = self._AddPolicyAttribute(dl, 'supported_on') + self._AddSupportedOnList(dd, policy['supported_on']) + dd = self._AddPolicyAttribute(dl, 'supported_features') + self._AddFeatures(dd, policy) + dd = self._AddPolicyAttribute(dl, 'description') + self._AddDescription(dd, policy) + if (self.IsPolicySupportedOnPlatform(policy, 'win') or + self.IsPolicySupportedOnPlatform(policy, 'linux') or + self.IsPolicySupportedOnPlatform(policy, 'android') or + self.IsPolicySupportedOnPlatform(policy, 'mac')): + # Don't add an example for ChromeOS-only policies. + if policy['type'] != 'external': + # All types except 'external' can be set through platform policy. + dd = self._AddPolicyAttribute(dl, 'example_value') + self._AddExample(dd, policy) + + def _AddPolicyNote(self, parent, policy): + '''If a policy has an additional web page assigned with it, then add + a link for that page. + + Args: + policy: The data structure of the policy. + ''' + if 'problem_href' not in policy: + return + problem_href = policy['problem_href'] + div = self._AddStyledElement(parent, 'div', ['div.note']) + note = self._GetLocalizedMessage('note').replace('$6', problem_href) + self._AddParagraphs(div, note) + + def _AddPolicyRow(self, parent, policy): + '''Adds a row for the policy in the summary table. + + Args: + parent: The DOM node of the summary table. + policy: The data structure of the policy. + ''' + tr = self._AddStyledElement(parent, 'tr', ['tr']) + indent = 'padding-left: %dpx;' % (7 + self._indent_level * 14) + if policy['type'] != 'group': + # Normal policies get two columns with name and caption. + name_td = self._AddStyledElement(tr, 'td', ['td', 'td.left'], + {'style': indent}) + self.AddElement(name_td, 'a', + {'href': '#' + policy['name']}, policy['name']) + self._AddStyledElement(tr, 'td', ['td', 'td.right'], {}, + policy['caption']) + else: + # Groups get one column with caption. + name_td = self._AddStyledElement(tr, 'td', ['td', 'td.left'], + {'style': indent, 'colspan': '2'}) + self.AddElement(name_td, 'a', {'href': '#' + policy['name']}, + policy['caption']) + + def _AddPolicySection(self, parent, policy): + '''Adds a section about the policy in the detailed policy listing. + + Args: + parent: The DOM node of the
of the detailed policy list. + policy: The data structure of the policy. + ''' + # Set style according to group nesting level. + indent = 'margin-left: %dpx' % (self._indent_level * 28) + if policy['type'] == 'group': + heading = 'h2' + else: + heading = 'h3' + parent2 = self.AddElement(parent, 'div', {'style': indent}) + + h2 = self.AddElement(parent2, heading) + self.AddElement(h2, 'a', {'name': policy['name']}) + if policy['type'] != 'group': + # Normal policies get a full description. + policy_name_text = policy['name'] + if 'deprecated' in policy and policy['deprecated'] == True: + policy_name_text += " (" + policy_name_text += self._GetLocalizedMessage('deprecated') + ")" + self.AddText(h2, policy_name_text) + self.AddElement(parent2, 'span', {}, policy['caption']) + self._AddPolicyNote(parent2, policy) + self._AddPolicyDetails(parent2, policy) + else: + # Groups get a more compact description. + self.AddText(h2, policy['caption']) + self._AddStyledElement(parent2, 'div', ['div.group_desc'], + {}, policy['desc']) + self.AddElement( + parent2, 'a', {'href': '#top'}, + self._GetLocalizedMessage('back_to_top')) + + # + # Implementation of abstract methods of TemplateWriter: + # + + def IsDeprecatedPolicySupported(self, policy): + return True + + def WritePolicy(self, policy): + self._AddPolicyRow(self._summary_tbody, policy) + self._AddPolicySection(self._details_div, policy) + + def BeginPolicyGroup(self, group): + self.WritePolicy(group) + self._indent_level += 1 + + def EndPolicyGroup(self): + self._indent_level -= 1 + + def BeginTemplate(self): + # Add a
for the summary section. + if self._GetChromiumVersionString() is not None: + self.AddComment(self._main_div, self.config['build'] + \ + ' version: ' + self._GetChromiumVersionString()) + + summary_div = self.AddElement(self._main_div, 'div') + self.AddElement(summary_div, 'a', {'name': 'top'}) + self.AddElement(summary_div, 'br') + self._AddParagraphs( + summary_div, + self._GetLocalizedMessage('intro')) + self.AddElement(summary_div, 'br') + self.AddElement(summary_div, 'br') + self.AddElement(summary_div, 'br') + # Add the summary table of policies. + summary_table = self._AddStyledElement(summary_div, 'table', ['table']) + # Add the first row. + thead = self.AddElement(summary_table, 'thead') + tr = self._AddStyledElement(thead, 'tr', ['tr']) + self._AddStyledElement( + tr, 'td', ['td', 'td.left', 'thead td'], {}, + self._GetLocalizedMessage('name_column_title')) + self._AddStyledElement( + tr, 'td', ['td', 'td.right', 'thead td'], {}, + self._GetLocalizedMessage('description_column_title')) + self._summary_tbody = self.AddElement(summary_table, 'tbody') + + # Add a
for the detailed policy listing. + self._details_div = self.AddElement(self._main_div, 'div') + + def Init(self): + dom_impl = minidom.getDOMImplementation('') + self._doc = dom_impl.createDocument(None, 'html', None) + body = self.AddElement(self._doc.documentElement, 'body') + self._main_div = self.AddElement(body, 'div') + self._indent_level = 0 + + # Human-readable names of supported platforms. + self._PLATFORM_MAP = { + 'win': 'Windows', + 'mac': 'Mac', + 'linux': 'Linux', + 'chrome_os': self.config['os_name'], + 'android': 'Android', + 'ios': 'iOS', + } + # Human-readable names of supported products. + self._PRODUCT_MAP = { + 'chrome': self.config['app_name'], + 'chrome_frame': self.config['frame_name'], + 'chrome_os': self.config['os_name'], + } + # Human-readable names of supported features. Each supported feature has + # a 'doc_feature_X' entry in |self.messages|. + self._FEATURE_MAP = {} + for message in self.messages: + if message.startswith('doc_feature_'): + self._FEATURE_MAP[message[12:]] = self.messages[message]['text'] + # Human-readable names of types. + self._TYPE_MAP = { + 'string': 'String', + 'int': 'Integer', + 'main': 'Boolean', + 'int-enum': 'Integer', + 'string-enum': 'String', + 'list': 'List of strings', + 'string-enum-list': 'List of strings', + 'dict': 'Dictionary', + 'external': 'External data reference', + } + self._REG_TYPE_MAP = { + 'string': 'REG_SZ', + 'int': 'REG_DWORD', + 'main': 'REG_DWORD', + 'int-enum': 'REG_DWORD', + 'string-enum': 'REG_SZ', + 'dict': 'REG_SZ', + } + self._RESTRICTION_TYPE_MAP = { + 'int-enum': 'choice', + 'string-enum': 'choice', + 'list': 'string', + 'string-enum-list': 'multi-select', + 'dict': 'string', + } + # The CSS style-sheet used for the document. It will be used in Google + # Sites, which strips class attributes from HTML tags. To work around this, + # the style-sheet is a dictionary and the style attributes will be added + # "by hand" for each element. + self._STYLE = { + 'table': 'border-style: none; border-collapse: collapse;', + 'tr': 'height: 0px;', + 'td': 'border: 1px dotted rgb(170, 170, 170); padding: 7px; ' + 'vertical-align: top; width: 236px; height: 15px;', + 'thead td': 'font-weight: bold;', + 'td.left': 'width: 200px;', + 'td.right': 'width: 100%;', + 'dt': 'font-weight: bold;', + 'dd dl': 'margin-top: 0px; margin-bottom: 0px;', + '.monospace': 'font-family: monospace;', + '.pre': 'white-space: pre;', + 'div.note': 'border: 2px solid black; padding: 5px; margin: 5px;', + 'div.group_desc': 'margin-top: 20px; margin-bottom: 20px;', + 'ul': 'padding-left: 0px; margin-left: 0px;' + } + + + def GetTemplateText(self): + # Return the text representation of the main
tag. + return self._main_div.toxml() + # To get a complete HTML file, use the following. + # return self._doc.toxml() diff --git a/tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py new file mode 100755 index 0000000..e51f9f0 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/doc_writer_unittest.py @@ -0,0 +1,951 @@ +#!/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.policy_templates.writers.doc_writer''' + + +import json +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest +from xml.dom import minidom + +from grit.format.policy_templates.writers import writer_unittest_common +from grit.format.policy_templates.writers import doc_writer + + +class MockMessageDictionary: + '''A mock dictionary passed to a writer as the dictionary of + localized messages. + ''' + + # Dictionary of messages. + msg_dict = {} + +class DocWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests for DocWriter.''' + + def setUp(self): + # Create a writer for the tests. + self.writer = doc_writer.GetWriter( + config={ + 'app_name': 'Chrome', + 'frame_name': 'Chrome Frame', + 'os_name': 'Chrome OS', + 'win_reg_mandatory_key_name': 'MockKey', + 'win_reg_recommended_key_name': 'MockKeyRec', + 'build': 'test_product', + }) + self.writer.messages = { + 'doc_back_to_top': {'text': '_test_back_to_top'}, + 'doc_complex_policies_on_windows': {'text': '_test_complex_policies_win'}, + 'doc_data_type': {'text': '_test_data_type'}, + 'doc_description': {'text': '_test_description'}, + 'doc_description_column_title': { + 'text': '_test_description_column_title' + }, + 'doc_example_value': {'text': '_test_example_value'}, + 'doc_feature_dynamic_refresh': {'text': '_test_feature_dynamic_refresh'}, + 'doc_feature_can_be_recommended': {'text': '_test_feature_recommended'}, + 'doc_feature_can_be_mandatory': {'text': '_test_feature_mandatory'}, + 'doc_intro': {'text': '_test_intro'}, + 'doc_mac_linux_pref_name': {'text': '_test_mac_linux_pref_name'}, + 'doc_android_restriction_name': { + 'text': '_test_android_restriction_name' + }, + 'doc_note': {'text': '_test_note'}, + 'doc_name_column_title': {'text': '_test_name_column_title'}, + 'doc_not_supported': {'text': '_test_not_supported'}, + 'doc_since_version': {'text': '_test_since_version'}, + 'doc_supported': {'text': '_test_supported'}, + 'doc_supported_features': {'text': '_test_supported_features'}, + 'doc_supported_on': {'text': '_test_supported_on'}, + 'doc_win_reg_loc': {'text': '_test_win_reg_loc'}, + + 'doc_bla': {'text': '_test_bla'}, + } + self.writer.Init() + + # It is not worth testing the exact content of style attributes. + # Therefore we override them here with shorter texts. + for key in self.writer._STYLE.keys(): + self.writer._STYLE[key] = 'style_%s;' % key + # Add some more style attributes for additional testing. + self.writer._STYLE['key1'] = 'style1;' + self.writer._STYLE['key2'] = 'style2;' + + # Create a DOM document for the tests. + dom_impl = minidom.getDOMImplementation('') + self.doc = dom_impl.createDocument(None, 'root', None) + self.doc_root = self.doc.documentElement + + def testSkeleton(self): + # Test if DocWriter creates the skeleton of the document correctly. + self.writer.BeginTemplate() + self.assertEquals( + self.writer._main_div.toxml(), + '
' + '' + '
' + '
') + + def testVersionAnnotation(self): + # Test if DocWriter creates the skeleton of the document correctly. + self.writer.config['version'] = '39.0.0.0' + self.writer.BeginTemplate() + self.assertEquals( + self.writer._main_div.toxml(), + '
' + '' + '' + '
' + '
') + + def testGetLocalizedMessage(self): + # Test if localized messages are retrieved correctly. + self.writer.messages = { + 'doc_hello_world': {'text': 'hello, vilag!'} + } + self.assertEquals( + self.writer._GetLocalizedMessage('hello_world'), + 'hello, vilag!') + + def testMapListToString(self): + # Test function DocWriter.MapListToString() + self.assertEquals( + self.writer._MapListToString({'a1': 'a2', 'b1': 'b2'}, ['a1', 'b1']), + 'a2, b2') + self.assertEquals( + self.writer._MapListToString({'a1': 'a2', 'b1': 'b2'}, []), + '') + result = self.writer._MapListToString( + {'a': '1', 'b': '2', 'c': '3', 'd': '4'}, ['b', 'd']) + expected_result = '2, 4' + self.assertEquals( + result, + expected_result) + + def testAddStyledElement(self): + # Test function DocWriter.AddStyledElement() + + # Test the case of zero style. + e1 = self.writer._AddStyledElement( + self.doc_root, 'z', [], {'a': 'b'}, 'text') + self.assertEquals( + e1.toxml(), + 'text') + + # Test the case of one style. + e2 = self.writer._AddStyledElement( + self.doc_root, 'z', ['key1'], {'a': 'b'}, 'text') + self.assertEquals( + e2.toxml(), + 'text') + + # Test the case of two styles. + e3 = self.writer._AddStyledElement( + self.doc_root, 'z', ['key1', 'key2'], {'a': 'b'}, 'text') + self.assertEquals( + e3.toxml(), + 'text') + + def testAddDescriptionIntEnum(self): + # Test if URLs are replaced and choices of 'int-enum' policies are listed + # correctly. + policy = { + 'type': 'int-enum', + 'items': [ + {'value': 0, 'caption': 'Disable foo'}, + {'value': 2, 'caption': 'Solve your problem'}, + {'value': 5, 'caption': 'Enable bar'}, + ], + 'desc': '''This policy disables foo, except in case of bar. +See http://policy-explanation.example.com for more details. +''' + } + self.writer._AddDescription(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '''

This policy disables foo, except in case of bar. +See http://policy-explanation.example.com for more details. +

  • 0 = Disable foo
  • 2 = Solve your problem
  • 5 = Enable bar
''') + + def testAddDescriptionStringEnum(self): + # Test if URLs are replaced and choices of 'int-enum' policies are listed + # correctly. + policy = { + 'type': 'string-enum', + 'items': [ + {'value': "one", 'caption': 'Disable foo'}, + {'value': "two", 'caption': 'Solve your problem'}, + {'value': "three", 'caption': 'Enable bar'}, + ], + 'desc': '''This policy disables foo, except in case of bar. +See http://policy-explanation.example.com for more details. +''' + } + self.writer._AddDescription(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '''

This policy disables foo, except in case of bar. +See http://policy-explanation.example.com for more details. +

  • "one" = Disable foo
  • "two" = Solve your problem
  • "three" = Enable bar
''') + + def testAddFeatures(self): + # Test if the list of features of a policy is handled correctly. + policy = { + 'features': { + 'spaceship_docking': False, + 'dynamic_refresh': True, + 'can_be_recommended': True, + } + } + self.writer._FEATURE_MAP = { + 'can_be_recommended': 'Can Be Recommended', + 'dynamic_refresh': 'Dynamic Refresh', + 'spaceship_docking': 'Spaceship Docking', + } + self.writer._AddFeatures(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + 'Can Be Recommended: _test_supported, ' + 'Dynamic Refresh: _test_supported, ' + 'Spaceship Docking: _test_not_supported' + '') + + def testAddListExample(self): + policy = { + 'name': 'PolicyName', + 'example_value': ['Foo', 'Bar'], + 'supported_on': [ { 'platforms': ['win', 'mac', 'linux'] } ] + } + self.writer._AddListExample(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '
' + '
Windows:
' + '
' + 'MockKey\\PolicyName\\1 = "Foo"\n' + 'MockKey\\PolicyName\\2 = "Bar"' + '
' + '
Android/Linux:
' + '
' + '["Foo", "Bar"]' + '
' + '
Mac:
' + '
' + '<array>\n' + ' <string>Foo</string>\n' + ' <string>Bar</string>\n' + '</array>' + '
' + '
' + '
') + + def testBoolExample(self): + # Test representation of boolean example values. + policy = { + 'name': 'PolicyName', + 'type': 'main', + 'example_value': True, + 'supported_on': [ { 'platforms': ['win', 'mac', 'linux', 'android'] } ] + } + e1 = self.writer.AddElement(self.doc_root, 'e1') + self.writer._AddExample(e1, policy) + self.assertEquals( + e1.toxml(), + '0x00000001 (Windows),' + ' true (Linux), true (Android),' + ' <true /> (Mac)') + + policy = { + 'name': 'PolicyName', + 'type': 'main', + 'example_value': False, + 'supported_on': [ { 'platforms': ['win', 'mac', 'linux', 'android'] } ] + } + e2 = self.writer.AddElement(self.doc_root, 'e2') + self.writer._AddExample(e2, policy) + self.assertEquals( + e2.toxml(), + '0x00000000 (Windows),' + ' false (Linux), false (Android),' + ' <false /> (Mac)') + + def testIntEnumExample(self): + # Test representation of 'int-enum' example values. + policy = { + 'name': 'PolicyName', + 'type': 'int-enum', + 'example_value': 16, + 'supported_on': [ { 'platforms': ['win', 'mac', 'linux', 'android'] } ] + } + self.writer._AddExample(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '0x00000010 (Windows), 16 (Linux), 16 (Android), 16 (Mac)') + + def testStringEnumExample(self): + # Test representation of 'string-enum' example values. + policy = { + 'name': 'PolicyName', + 'type': 'string-enum', + 'example_value': "wacky" + } + self.writer._AddExample(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '"wacky"') + + def testListExample(self): + # Test representation of 'list' example values. + policy = { + 'name': 'PolicyName', + 'type': 'list', + 'example_value': ['one', 'two'], + 'supported_on': [ { 'platforms': ['linux'] } ] + } + self.writer._AddExample(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '
' + '
Android/Linux:
' + '
' + '["one", "two"]' + '
') + + def testStringEnumListExample(self): + # Test representation of 'string-enum-list' example values. + policy = { + 'name': 'PolicyName', + 'type': 'string-enum-list', + 'example_value': ['one', 'two'], + 'supported_on': [ { 'platforms': ['linux'] } ] + } + self.writer._AddExample(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '
' + '
Android/Linux:
' + '
' + '["one", "two"]' + '
') + + def testStringExample(self): + # Test representation of 'string' example values. + policy = { + 'name': 'PolicyName', + 'type': 'string', + 'example_value': 'awesome-example' + } + self.writer._AddExample(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '"awesome-example"') + + def testIntExample(self): + # Test representation of 'int' example values. + policy = { + 'name': 'PolicyName', + 'type': 'int', + 'example_value': 26, + 'supported_on': [ { 'platforms': ['win', 'mac', 'linux', 'android'] } ] + } + self.writer._AddExample(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '0x0000001a (Windows), 26 (Linux), 26 (Android), 26 (Mac)') + + def testAddPolicyAttribute(self): + # Test creating a policy attribute term-definition pair. + self.writer._AddPolicyAttribute( + self.doc_root, 'bla', 'hello, world', ['key1']) + self.assertEquals( + self.doc_root.toxml(), + '' + '
_test_bla
' + '
hello, world
' + '') + + def testAddPolicyDetails(self): + # Test if the definition list (
) of policy details is created correctly. + policy = { + 'type': 'main', + 'name': 'TestPolicyName', + 'caption': 'TestPolicyCaption', + 'desc': 'TestPolicyDesc', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['win', 'mac', 'linux'], + 'since_version': '8', + 'until_version': '', + }, { + 'product': 'chrome', + 'platforms': ['android'], + 'since_version': '30', + 'until_version': '', + }, { + 'product': 'chrome', + 'platforms': ['ios'], + 'since_version': '34', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': False + } + self.writer.messages['doc_since_version'] = {'text': '...$6...'} + self.writer._AddPolicyDetails(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '
' + '
_test_data_type
' + '
Boolean [Windows:REG_DWORD]
' + '
_test_win_reg_loc
' + '
MockKey\TestPolicyName
' + '
_test_mac_linux_pref_name
' + '
TestPolicyName
' + '
_test_android_restriction_name
' + '
TestPolicyName
' + '
_test_supported_on
' + '
' + '
    ' + '
  • Chrome (Windows, Mac, Linux) ...8...
  • ' + '
  • Chrome (Android) ...30...
  • ' + '
  • Chrome (iOS) ...34...
  • ' + '
' + '
' + '
_test_supported_features
' + '
_test_feature_dynamic_refresh: _test_not_supported
' + '
_test_description

TestPolicyDesc

' + '
_test_example_value
' + '
0x00000000 (Windows), false (Linux),' + ' false (Android), <false /> (Mac)
' + '
') + + def testAddDictPolicyDetails(self): + # Test if the definition list (
) of policy details is created correctly + # for 'dict' policies. + policy = { + 'type': 'dict', + 'name': 'TestPolicyName', + 'caption': 'TestPolicyCaption', + 'desc': 'TestPolicyDesc', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['win', 'mac', 'linux'], + 'since_version': '8', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': { 'foo': 123 } + } + self.writer.messages['doc_since_version'] = {'text': '...$6...'} + self.writer._AddPolicyDetails(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '
' + '
_test_data_type
' + '
Dictionary [Windows:REG_SZ] (_test_complex_policies_win)
' + '
_test_win_reg_loc
' + '
MockKey\TestPolicyName
' + '
_test_mac_linux_pref_name
' + '
TestPolicyName
' + '
_test_supported_on
' + '
' + '
    ' + '
  • Chrome (Windows, Mac, Linux) ...8...
  • ' + '
' + '
' + '
_test_supported_features
' + '
_test_feature_dynamic_refresh: _test_not_supported
' + '
_test_description

TestPolicyDesc

' + '
_test_example_value
' + '
' + '
' + '
Windows:
' + '
MockKey\TestPolicyName = {"foo": 123}
' + '
Android/Linux:
' + '
TestPolicyName: {"foo": 123}
' + '
Mac:
' + '
' + '<key>TestPolicyName</key>\n' + '<dict>\n' + ' <key>foo</key>\n' + ' <integer>123</integer>\n' + '</dict>' + '
' + '
' + '
' + '
') + + def testAddPolicyDetailsRecommendedOnly(self): + policy = { + 'type': 'main', + 'name': 'TestPolicyName', + 'caption': 'TestPolicyCaption', + 'desc': 'TestPolicyDesc', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['win', 'mac', 'linux'], + 'since_version': '8', + 'until_version': '', + }, { + 'product': 'chrome', + 'platforms': ['android'], + 'since_version': '30', + 'until_version': '', + }, { + 'product': 'chrome', + 'platforms': ['ios'], + 'since_version': '34', + 'until_version': '', + }], + 'features': { + 'dynamic_refresh': False, + 'can_be_mandatory': False, + 'can_be_recommended': True + }, + 'example_value': False + } + self.writer.messages['doc_since_version'] = {'text': '...$6...'} + self.writer._AddPolicyDetails(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '
' + '
_test_data_type
' + '
Boolean [Windows:REG_DWORD]
' + '
_test_win_reg_loc
' + '
MockKeyRec\TestPolicyName
' + '
_test_mac_linux_pref_name
' + '
TestPolicyName
' + '
_test_android_restriction_name
' + '
TestPolicyName
' + '
_test_supported_on
' + '
' + '
    ' + '
  • Chrome (Windows, Mac, Linux) ...8...
  • ' + '
  • Chrome (Android) ...30...
  • ' + '
  • Chrome (iOS) ...34...
  • ' + '
' + '
' + '
_test_supported_features
' + '
_test_feature_mandatory: _test_not_supported,' + ' _test_feature_recommended: _test_supported,' + ' _test_feature_dynamic_refresh: _test_not_supported
' + '
_test_description

TestPolicyDesc

' + '
_test_example_value
' + '
0x00000000 (Windows), false (Linux),' + ' false (Android), <false /> (Mac)
' + '
') + + def testAddPolicyNote(self): + # TODO(jkummerow): The functionality tested by this test is currently not + # used for anything and will probably soon be removed. + # Test if nodes are correctly added to policies. + policy = { + 'problem_href': 'http://www.example.com/5' + } + self.writer.messages['doc_note'] = {'text': '...$6...'} + self.writer._AddPolicyNote(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '

...' + 'http://www.example.com/5' + '...

') + + def testAddPolicyRow(self): + # Test if policies are correctly added to the summary table. + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'type': 'string', + } + self.writer._indent_level = 3 + self.writer._AddPolicyRow(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + 'PolicyName' + '' + 'PolicyCaption' + '') + self.setUp() + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'type': 'group', + } + self.writer._indent_level = 2 + self.writer._AddPolicyRow(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + 'PolicyCaption' + '' + '') + + def testAddPolicySection(self): + # Test if policy details are correctly added to the document. + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'desc': 'PolicyDesc', + 'type': 'string', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['win', 'mac'], + 'since_version': '7', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': 'False' + } + self.writer.messages['doc_since_version'] = {'text': '..$6..'} + self.writer._AddPolicySection(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + '') + # Test for groups. + self.setUp() + policy['type'] = 'group' + self.writer._AddPolicySection(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + '') + + def testAddPolicySectionForWindowsOnly(self): + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'desc': 'PolicyDesc', + 'type': 'int', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['win'], + 'since_version': '33', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': 123 + } + self.writer.messages['doc_since_version'] = {'text': '..$6..'} + self.writer._AddPolicySection(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + '') + + def testAddPolicySectionForMacOnly(self): + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'desc': 'PolicyDesc', + 'type': 'int', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['mac'], + 'since_version': '33', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': 123 + } + self.writer.messages['doc_since_version'] = {'text': '..$6..'} + self.writer._AddPolicySection(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + '') + + def testAddPolicySectionForLinuxOnly(self): + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'desc': 'PolicyDesc', + 'type': 'int', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['linux'], + 'since_version': '33', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': 123 + } + self.writer.messages['doc_since_version'] = {'text': '..$6..'} + self.writer._AddPolicySection(self.doc_root, policy) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + '') + + def testAddPolicySectionForAndroidOnly(self): + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'desc': 'PolicyDesc', + 'type': 'int', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['android'], + 'since_version': '33', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': 123 + } + self.writer.messages['doc_since_version'] = {'text': '..$6..'} + self.writer._AddPolicySection(self.doc_root, policy) + self.assertTrue(self.writer.IsPolicySupportedOnPlatform(policy, 'android')) + self.assertEquals( + self.doc_root.toxml(), + '' + '' + '') + + def testAddDictionaryExample(self): + policy = { + 'name': 'PolicyName', + 'caption': 'PolicyCaption', + 'desc': 'PolicyDesc', + 'type': 'dict', + 'supported_on': [{ + 'product': 'chrome', + 'platforms': ['win', 'mac', 'linux'], + 'since_version': '7', + 'until_version': '', + }], + 'features': {'dynamic_refresh': False}, + 'example_value': { + "ProxyMode": "direct", + "List": ["1", "2", "3"], + "True": True, + "False": False, + "Integer": 123, + "DictList": [ { + "A": 1, + "B": 2, + }, { + "C": 3, + "D": 4, + }, + ], + }, + } + self.writer._AddDictionaryExample(self.doc_root, policy) + value = json.dumps(policy['example_value']).replace('"', '"') + self.assertEquals( + self.doc_root.toxml(), + '' + '
' + '
Windows:
' + '
MockKey\PolicyName = ' + + value + + '
' + '
Android/Linux:
' + '
PolicyName: ' + value + '
' + '
Mac:
' + '
' + '<key>PolicyName</key>\n' + '<dict>\n' + ' <key>DictList</key>\n' + ' <array>\n' + ' <dict>\n' + ' <key>A</key>\n' + ' <integer>1</integer>\n' + ' <key>B</key>\n' + ' <integer>2</integer>\n' + ' </dict>\n' + ' <dict>\n' + ' <key>C</key>\n' + ' <integer>3</integer>\n' + ' <key>D</key>\n' + ' <integer>4</integer>\n' + ' </dict>\n' + ' </array>\n' + ' <key>False</key>\n' + ' <false/>\n' + ' <key>Integer</key>\n' + ' <integer>123</integer>\n' + ' <key>List</key>\n' + ' <array>\n' + ' <string>1</string>\n' + ' <string>2</string>\n' + ' <string>3</string>\n' + ' </array>\n' + ' <key>ProxyMode</key>\n' + ' <string>direct</string>\n' + ' <key>True</key>\n' + ' <true/>\n' + '</dict>' + '
' + '
' + '
') + + def testParagraphs(self): + text = 'Paragraph 1\n\nParagraph 2\n\nParagraph 3' + self.writer._AddParagraphs(self.doc_root, text) + self.assertEquals( + self.doc_root.toxml(), + '

Paragraph 1

Paragraph 2

Paragraph 3

') + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/ios_plist_writer.py b/tools/grit/grit/format/policy_templates/writers/ios_plist_writer.py new file mode 100755 index 0000000..b726ffa --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/ios_plist_writer.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# Copyright (c) 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. + + +import base64 + +from xml.dom import minidom +from grit.format.policy_templates.writers import plist_writer + + +# This writer outputs a Property List with an example for each of the policies +# supported on iOS. This plist can be pushed to Chrome on iOS via the MDM API +# introduced in iOS 7. + +CHROME_POLICY_COMMENT = '''\ + ChromePolicy is the preferred key to configure Chrome. + Each of the keys in this configures a Chrome policy. + All of the Chrome policies are configured with an example + value below. + Note that it's not necessary to configure all of them. ''' + +ENCODED_CHROME_POLICY_COMMENT = '''\ + EncodedChromePolicy contains a Property List file, encoded in Base64, + which contains the same policies that can go in ChromePolicy. + This key can be used by vendors that restrict the app configuration + types to strings. + The value of this string can be validated by running these + commands in Mac OS X: + + # (first, copy-paste the string into a file named "policy.plist") + # base64 -D < policy.plist > decoded_policy.plist + # plutil -lint decoded_policy.plist + + plutil should indicate that decoded_policy.plist is valid, + otherwise Chrome will reject the encoded string too. + + This command can be used to pretty-print the plist file: + + # plutil -convert xml1 decoded_policy.plist + + Note that is the preferred key to configure Chrome. + If is present then is ignored. ''' + +def GetWriter(config): + '''Factory method for creating IOSPlistWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return IOSPlistWriter(['ios'], config) + + +class IOSPlistWriter(plist_writer.PListWriter): + '''Class for generating policy templates in the iOS plist format. + It is used by PolicyTemplateGenerator to write plist files. + ''' + + # Overridden. + def IsPolicySupported(self, policy): + # Output examples only for policies that are supported on iOS. + for support_on in policy['supported_on']: + if ('ios' in support_on['platforms'] and + support_on['until_version'] == '' and + super(IOSPlistWriter, self).IsPolicySupported(policy)): + return True + return False + + def _WriteValue(self, parent, value): + if type(value) == bool: + self.AddElement(parent, 'true' if value else 'false') + elif type(value) == int: + self.AddElement(parent, 'integer', {}, str(value)) + elif type(value) == str: + self.AddElement(parent, 'string', {}, value) + elif type(value) == list: + array = self.AddElement(parent, 'array') + for element in value: + self._WriteValue(array, element) + elif type(value) == dict: + dic = self.AddElement(parent, 'dict') + for k, v in sorted(value.iteritems()): + self.AddElement(dic, 'key', {}, k) + self._WriteValue(dic, v) + else: + raise ValueError('Unsupported type in example value: ' + type(value)) + + # Overridden. + def WritePolicy(self, policy): + for dict in [self._dict, self._encoded_dict]: + self.AddElement(dict, 'key', {}, policy['name']) + self._WriteValue(dict, policy['example_value']) + + # Overridden. + # |self._plist| is created in super.Init(). + def BeginTemplate(self): + self._plist.attributes['version'] = '1.0' + self._root_dict = self.AddElement(self._plist, 'dict') + self.AddComment(self._root_dict, CHROME_POLICY_COMMENT) + if self._GetChromiumVersionString() is not None: + self.AddComment(self._root_dict, ' ' + self.config['build'] + \ + ' version: ' + self._GetChromiumVersionString() + ' ') + self._dict = self._AddKeyValuePair(self._root_dict, 'ChromePolicy', 'dict') + + self._encoded_plist.attributes['version'] = '1.0' + self._encoded_dict = self.AddElement(self._encoded_plist, 'dict') + + # Overridden. + def EndTemplate(self): + # Add the "EncodedChromePolicy" entry. + encoded = base64.b64encode(self._encoded_doc.toxml()) + self.AddComment(self._root_dict, ENCODED_CHROME_POLICY_COMMENT) + self._AddStringKeyValuePair(self._root_dict, 'EncodedChromePolicy', encoded) + + # Overridden. + def Init(self): + super(IOSPlistWriter, self).Init() + # Create a secondary DOM for the EncodedChromePolicy Plist, which will be + # serialized and encoded in EndTemplate. + self._encoded_doc = self.CreatePlistDocument() + self._encoded_plist = self._encoded_doc.documentElement + + # Overridden. + def GetTemplateText(self): + return self.ToPrettyXml(self._doc, encoding='UTF-8') diff --git a/tools/grit/grit/format/policy_templates/writers/ios_plist_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/ios_plist_writer_unittest.py new file mode 100755 index 0000000..0fdecb1 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/ios_plist_writer_unittest.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# Copyright (c) 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. + +'''Unit tests for grit.format.policy_templates.writers.ios_plist_writer''' + + +import base64 +import functools +import os +import plistlib +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest + +try: + import Cocoa +except: + Cocoa = None + +from grit.format.policy_templates.writers import writer_unittest_common + + +class IOSPListWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests for IOSPListWriter.''' + + def _ParseWithPython(self, decode, text): + '''Parses a serialized Plist, using Python's plistlib. + + If |decode| is true then |text| is decoded as Base64 before being + deserialized as a Plist.''' + if decode: + text = base64.b64decode(text) + return plistlib.readPlistFromString(text) + + def _ParseWithCocoa(self, decode, text): + '''Parses a serialized Plist, using Cocoa's python bindings. + + If |decode| is true then |text| is decoded as Base64 before being + deserialized as a Plist.''' + if decode: + data = Cocoa.NSData.alloc().initWithBase64EncodedString_options_(text, 0) + else: + data = Cocoa.NSData.alloc().initWithBytes_length_(text, len(text)) + result = Cocoa.NSPropertyListSerialization. \ + propertyListFromData_mutabilityOption_format_errorDescription_( + data, Cocoa.NSPropertyListImmutable, None, None) + return result[0] + + def _VerifyGeneratedOutputWithParsers(self, + templates, + expected_output, + parse, + decode_and_parse): + + + _defines = { '_chromium': '1', + 'mac_bundle_id': 'com.example.Test', + 'version': '39.0.0.0' } + + # Generate the grit output for |templates|. + output = self.GetOutput( + self.PrepareTest(templates), + 'fr', + _defines, + 'ios_plist', + 'en') + + # Parse it as a Plist. + plist = parse(output) + self.assertEquals(len(plist), 2) + self.assertTrue('ChromePolicy' in plist) + self.assertTrue('EncodedChromePolicy' in plist) + + # Get the 2 expected fields. + chrome_policy = plist['ChromePolicy'] + encoded_chrome_policy = plist['EncodedChromePolicy'] + + # Verify the ChromePolicy. + self.assertEquals(chrome_policy, expected_output) + + # Decode the EncodedChromePolicy and verify it. + decoded_chrome_policy = decode_and_parse(encoded_chrome_policy) + self.assertEquals(decoded_chrome_policy, expected_output) + + def _VerifyGeneratedOutput(self, templates, expected): + # plistlib is available on all Python platforms. + parse = functools.partial(self._ParseWithPython, False) + decode_and_parse = functools.partial(self._ParseWithPython, True) + self._VerifyGeneratedOutputWithParsers( + templates, expected, parse, decode_and_parse) + + # The Cocoa bindings are available on Mac OS X only. + if Cocoa: + parse = functools.partial(self._ParseWithCocoa, False) + decode_and_parse = functools.partial(self._ParseWithCocoa, True) + self._VerifyGeneratedOutputWithParsers( + templates, expected, parse, decode_and_parse) + + def _MakeTemplate(self, name, type, example, extra=''): + return ''' + { + 'policy_definitions': [ + { + 'name': '%s', + 'type': '%s', + 'desc': '', + 'caption': '', + 'supported_on': ['ios:35-'], + 'example_value': %s, + %s + }, + ], + 'placeholders': [], + 'messages': {}, + } + ''' % (name, type, example, extra) + + def testEmpty(self): + templates = ''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': {}, + } + ''' + expected = {} + self._VerifyGeneratedOutput(templates, expected) + + def testEmptyVersion(self): + templates = ''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': {}, + } + ''' + expected = {} + self._VerifyGeneratedOutput(templates, expected) + + def testBoolean(self): + templates = self._MakeTemplate('BooleanPolicy', 'main', 'True') + expected = { + 'BooleanPolicy': True, + } + self._VerifyGeneratedOutput(templates, expected) + + def testString(self): + templates = self._MakeTemplate('StringPolicy', 'string', '"Foo"') + expected = { + 'StringPolicy': 'Foo', + } + self._VerifyGeneratedOutput(templates, expected) + + def testStringEnum(self): + templates = self._MakeTemplate( + 'StringEnumPolicy', 'string-enum', '"Foo"', + ''' + 'items': [ + { 'name': 'Foo', 'value': 'Foo', 'caption': '' }, + { 'name': 'Bar', 'value': 'Bar', 'caption': '' }, + ], + ''') + expected = { + 'StringEnumPolicy': 'Foo', + } + self._VerifyGeneratedOutput(templates, expected) + + def testInt(self): + templates = self._MakeTemplate('IntPolicy', 'int', '42') + expected = { + 'IntPolicy': 42, + } + self._VerifyGeneratedOutput(templates, expected) + + def testIntEnum(self): + templates = self._MakeTemplate( + 'IntEnumPolicy', 'int-enum', '42', + ''' + 'items': [ + { 'name': 'Foo', 'value': 100, 'caption': '' }, + { 'name': 'Bar', 'value': 42, 'caption': '' }, + ], + ''') + expected = { + 'IntEnumPolicy': 42, + } + self._VerifyGeneratedOutput(templates, expected) + + def testStringList(self): + templates = self._MakeTemplate('StringListPolicy', 'list', '["a", "b"]') + expected = { + 'StringListPolicy': [ "a", "b" ], + } + self._VerifyGeneratedOutput(templates, expected) + + def testStringEnumList(self): + templates = self._MakeTemplate('StringEnumListPolicy', + 'string-enum-list', '["a", "b"]', + ''' + 'items': [ + { 'name': 'Foo', 'value': 'a', 'caption': '' }, + { 'name': 'Bar', 'value': 'b', 'caption': '' }, + ], + ''') + + expected = { + 'StringEnumListPolicy': [ "a", "b" ], + } + self._VerifyGeneratedOutput(templates, expected) + + def testListOfDictionary(self): + templates = self._MakeTemplate( + 'ManagedBookmarks', 'dict', + ''' + [ + { + "name": "Google Search", + "url": "www.google.com", + }, + { + "name": "Youtube", + "url": "www.youtube.com", + } + ] + ''') + expected = { + 'ManagedBookmarks': [ + { "name": "Google Search", "url": "www.google.com" }, + { "name": "Youtube", "url": "www.youtube.com" }, + ], + } + self._VerifyGeneratedOutput(templates, expected) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/json_writer.py b/tools/grit/grit/format/policy_templates/writers/json_writer.py new file mode 100755 index 0000000..4dfd282 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/json_writer.py @@ -0,0 +1,95 @@ +#!/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. + +import json + +from textwrap import TextWrapper +from grit.format.policy_templates.writers import template_writer + + +TEMPLATE_HEADER="""\ +// Policy template for Linux. +// Uncomment the policies you wish to activate and change their values to +// something useful for your case. The provided values are for reference only +// and do not provide meaningful defaults! +{""" + + +HEADER_DELIMETER="""\ + //-------------------------------------------------------------------------""" + + +def GetWriter(config): + '''Factory method for creating JsonWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return JsonWriter(['linux'], config) + + +class JsonWriter(template_writer.TemplateWriter): + '''Class for generating policy files in JSON format (for Linux). The + generated files will define all the supported policies with example values + set for them. This class is used by PolicyTemplateGenerator to write .json + files. + ''' + + def PreprocessPolicies(self, policy_list): + return self.FlattenGroupsAndSortPolicies(policy_list) + + def WriteComment(self, comment): + self._out.append('// ' + comment) + + def WritePolicy(self, policy): + if policy['type'] == 'external': + # This type can only be set through cloud policy. + return + example_value_str = json.dumps(policy['example_value'], sort_keys=True) + + # Add comma to the end of the previous line. + if not self._first_written: + self._out[-2] += ',' + + if not self.CanBeMandatory(policy) and self.CanBeRecommended(policy): + line = ' // Note: this policy is supported only in recommended mode.' + self._out.append(line) + line = ' // The JSON file should be placed in %srecommended.' % \ + self.config['linux_policy_path'] + self._out.append(line) + + line = ' // %s' % policy['caption'] + self._out.append(line) + self._out.append(HEADER_DELIMETER) + description = self._text_wrapper.wrap(policy['desc']) + self._out += description; + line = ' //"%s": %s' % (policy['name'], example_value_str) + self._out.append('') + self._out.append(line) + self._out.append('') + + self._first_written = False + + def BeginTemplate(self): + if self._GetChromiumVersionString() is not None: + self.WriteComment(self.config['build'] + ''' version: ''' + \ + self._GetChromiumVersionString()) + self._out.append(TEMPLATE_HEADER) + + def EndTemplate(self): + self._out.append('}') + + def Init(self): + self._out = [] + # The following boolean member is true until the first policy is written. + self._first_written = True + # Create the TextWrapper object once. + self._text_wrapper = TextWrapper( + initial_indent = ' // ', + subsequent_indent = ' // ', + break_long_words = False, + width = 80) + + def GetTemplateText(self): + return '\n'.join(self._out) diff --git a/tools/grit/grit/format/policy_templates/writers/json_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/json_writer_unittest.py new file mode 100755 index 0000000..8f3c745 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/json_writer_unittest.py @@ -0,0 +1,429 @@ +#!/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.policy_templates.writers.json_writer''' + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest + +from grit.format.policy_templates.writers import writer_unittest_common + + +TEMPLATE_HEADER="""\ +// Policy template for Linux. +// Uncomment the policies you wish to activate and change their values to +// something useful for your case. The provided values are for reference only +// and do not provide meaningful defaults! +{ +""" + +TEMPLATE_HEADER_WITH_VERSION="""\ +// chromium version: 39.0.0.0 +// Policy template for Linux. +// Uncomment the policies you wish to activate and change their values to +// something useful for your case. The provided values are for reference only +// and do not provide meaningful defaults! +{ +""" + + +HEADER_DELIMETER="""\ + //------------------------------------------------------------------------- +""" + + +class JsonWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests for JsonWriter.''' + + def CompareOutputs(self, output, expected_output): + '''Compares the output of the json_writer with its expected output. + + Args: + output: The output of the json writer as returned by grit. + expected_output: The expected output. + + Raises: + AssertionError: if the two strings are not equivalent. + ''' + self.assertEquals( + output.strip(), + expected_output.strip()) + + def testEmpty(self): + # Test the handling of an empty policy list. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium': '1'}, 'json', 'en') + expected_output = TEMPLATE_HEADER + '}' + self.CompareOutputs(output, expected_output) + + def testEmptyWithVersion(self): + # Test the handling of an empty policy list. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput( + grd, 'fr', {'_chromium': '1', 'version':'39.0.0.0'}, 'json', 'en') + expected_output = TEMPLATE_HEADER_WITH_VERSION + '}' + self.CompareOutputs(output, expected_output) + + def testMainPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "MainPolicy",' + ' "type": "main",' + ' "caption": "Example Main Policy",' + ' "desc": "Example Main Policy",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": True' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example Main Policy\n' + + HEADER_DELIMETER + + ' // Example Main Policy\n\n' + ' //"MainPolicy": true\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testRecommendedOnlyPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "MainPolicy",' + ' "type": "main",' + ' "caption": "Example Main Policy",' + ' "desc": "Example Main Policy",' + ' "features": {' + ' "can_be_recommended": True,' + ' "can_be_mandatory": False' + ' },' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": True' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Note: this policy is supported only in recommended mode.\n' + + ' // The JSON file should be placed in' + + ' /etc/opt/chrome/policies/recommended.\n' + + ' // Example Main Policy\n' + + HEADER_DELIMETER + + ' // Example Main Policy\n\n' + ' //"MainPolicy": true\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testStringPolicy(self): + # Tests a policy group with a single policy of type 'string'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "StringPolicy",' + ' "type": "string",' + ' "caption": "Example String Policy",' + ' "desc": "Example String Policy",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": "hello, world!"' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example String Policy\n' + + HEADER_DELIMETER + + ' // Example String Policy\n\n' + ' //"StringPolicy": "hello, world!"\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testIntPolicy(self): + # Tests a policy group with a single policy of type 'string'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "IntPolicy",' + ' "type": "int",' + ' "caption": "Example Int Policy",' + ' "desc": "Example Int Policy",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": 15' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example Int Policy\n' + + HEADER_DELIMETER + + ' // Example Int Policy\n\n' + ' //"IntPolicy": 15\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testIntEnumPolicy(self): + # Tests a policy group with a single policy of type 'int-enum'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "EnumPolicy",' + ' "type": "int-enum",' + ' "caption": "Example Int Enum",' + ' "desc": "Example Int Enum",' + ' "items": [' + ' {"name": "ProxyServerDisabled", "value": 0, "caption": ""},' + ' {"name": "ProxyServerAutoDetect", "value": 1, "caption": ""},' + ' ],' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": 1' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome': '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example Int Enum\n' + + HEADER_DELIMETER + + ' // Example Int Enum\n\n' + ' //"EnumPolicy": 1\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testStringEnumPolicy(self): + # Tests a policy group with a single policy of type 'string-enum'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "EnumPolicy",' + ' "type": "string-enum",' + ' "caption": "Example String Enum",' + ' "desc": "Example String Enum",' + ' "items": [' + ' {"name": "ProxyServerDisabled", "value": "one",' + ' "caption": ""},' + ' {"name": "ProxyServerAutoDetect", "value": "two",' + ' "caption": ""},' + ' ],' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": "one"' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome': '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example String Enum\n' + + HEADER_DELIMETER + + ' // Example String Enum\n\n' + ' //"EnumPolicy": "one"\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testListPolicy(self): + # Tests a policy group with a single policy of type 'list'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "ListPolicy",' + ' "type": "list",' + ' "caption": "Example List",' + ' "desc": "Example List",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": ["foo", "bar"]' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example List\n' + + HEADER_DELIMETER + + ' // Example List\n\n' + ' //"ListPolicy": ["foo", "bar"]\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testStringEnumListPolicy(self): + # Tests a policy group with a single policy of type 'string-enum-list'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "ListPolicy",' + ' "type": "string-enum-list",' + ' "caption": "Example List",' + ' "desc": "Example List",' + ' "items": [' + ' {"name": "ProxyServerDisabled", "value": "one",' + ' "caption": ""},' + ' {"name": "ProxyServerAutoDetect", "value": "two",' + ' "caption": ""},' + ' ],' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": ["one", "two"]' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example List\n' + + HEADER_DELIMETER + + ' // Example List\n\n' + ' //"ListPolicy": ["one", "two"]\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testDictionaryPolicy(self): + # Tests a policy group with a single policy of type 'dict'. + example = { + 'bool': True, + 'dict': { + 'a': 1, + 'b': 2, + }, + 'int': 10, + 'list': [1, 2, 3], + 'string': 'abc', + } + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "DictionaryPolicy",' + ' "type": "dict",' + ' "caption": "Example Dictionary Policy",' + ' "desc": "Example Dictionary Policy",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": ' + str(example) + + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Example Dictionary Policy\n' + + HEADER_DELIMETER + + ' // Example Dictionary Policy\n\n' + ' //"DictionaryPolicy": {"bool": true, "dict": {"a": 1, ' + '"b": 2}, "int": 10, "list": [1, 2, 3], "string": "abc"}\n\n' + '}') + self.CompareOutputs(output, expected_output) + + def testNonSupportedPolicy(self): + # Tests a policy that is not supported on Linux, so it shouldn't + # be included in the JSON file. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "NonLinuxPolicy",' + ' "type": "list",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.mac:8-"],' + ' "example_value": ["a"]' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'json', 'en') + expected_output = TEMPLATE_HEADER + '}' + self.CompareOutputs(output, expected_output) + + def testPolicyGroup(self): + # Tests a policy group that has more than one policies. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "Group1",' + ' "type": "group",' + ' "caption": "",' + ' "desc": "",' + ' "policies": [{' + ' "name": "Policy1",' + ' "type": "list",' + ' "caption": "Policy One",' + ' "desc": "Policy One",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": ["a", "b"]' + ' },{' + ' "name": "Policy2",' + ' "type": "string",' + ' "caption": "Policy Two",' + ' "desc": "Policy Two",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": "c"' + ' }],' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'json', 'en') + expected_output = ( + TEMPLATE_HEADER + + ' // Policy One\n' + + HEADER_DELIMETER + + ' // Policy One\n\n' + ' //"Policy1": ["a", "b"],\n\n' + ' // Policy Two\n' + + HEADER_DELIMETER + + ' // Policy Two\n\n' + ' //"Policy2": "c"\n\n' + '}') + self.CompareOutputs(output, expected_output) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/mock_writer.py b/tools/grit/grit/format/policy_templates/writers/mock_writer.py new file mode 100755 index 0000000..3db3a54 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/mock_writer.py @@ -0,0 +1,30 @@ +#!/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. + + +from template_writer import TemplateWriter + + +class MockWriter(TemplateWriter): + '''Helper class for unit tests in policy_template_generator_unittest.py + ''' + + def __init__(self): + pass + + def WritePolicy(self, policy): + pass + + def BeginTemplate(self): + pass + + def GetTemplateText(self): + pass + + def IsPolicySupported(self, policy): + return True + + def Test(self): + pass diff --git a/tools/grit/grit/format/policy_templates/writers/plist_helper.py b/tools/grit/grit/format/policy_templates/writers/plist_helper.py new file mode 100755 index 0000000..0c599ca --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/plist_helper.py @@ -0,0 +1,15 @@ +#!/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. + + +'''Common functions for plist_writer and plist_strings_writer. +''' + + +def GetPlistFriendlyName(name): + '''Transforms a string so that it will be suitable for use as + a pfm_name in the plist manifest file. + ''' + return name.replace(' ', '_') diff --git a/tools/grit/grit/format/policy_templates/writers/plist_strings_writer.py b/tools/grit/grit/format/policy_templates/writers/plist_strings_writer.py new file mode 100755 index 0000000..4257bf8 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/plist_strings_writer.py @@ -0,0 +1,82 @@ +#!/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. + + +from grit.format.policy_templates.writers import plist_helper +from grit.format.policy_templates.writers import template_writer + + +def GetWriter(config): + '''Factory method for creating PListStringsWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return PListStringsWriter(['mac'], config) + + +class PListStringsWriter(template_writer.TemplateWriter): + '''Outputs localized string table files for the Mac policy file. + These files are named Localizable.strings and they are in the + [lang].lproj subdirectories of the manifest bundle. + ''' + + def WriteComment(self, comment): + self._out.append('/* ' + comment + ' */' ) + + def _AddToStringTable(self, item_name, caption, desc): + '''Add a title and a description of an item to the string table. + + Args: + item_name: The name of the item that will get the title and the + description. + title: The text of the title to add. + desc: The text of the description to add. + ''' + caption = caption.replace('"', '\\"') + caption = caption.replace('\n', '\\n') + desc = desc.replace('"', '\\"') + desc = desc.replace('\n', '\\n') + self._out.append('%s.pfm_title = \"%s\";' % (item_name, caption)) + self._out.append('%s.pfm_description = \"%s\";' % (item_name, desc)) + + def PreprocessPolicies(self, policy_list): + return self.FlattenGroupsAndSortPolicies(policy_list) + + def WritePolicy(self, policy): + '''Add strings to the stringtable corresponding a given policy. + + Args: + policy: The policy for which the strings will be added to the + string table. + ''' + desc = policy['desc'] + if policy['type'] == 'external': + # This type can only be set through cloud policy. + return + elif policy['type'] in ('int-enum','string-enum', 'string-enum-list'): + # Append the captions of enum items to the description string. + item_descs = [] + for item in policy['items']: + item_descs.append(str(item['value']) + ' - ' + item['caption']) + desc = '\n'.join(item_descs) + '\n' + desc + + self._AddToStringTable(policy['name'], policy['label'], desc) + + def BeginTemplate(self): + app_name = plist_helper.GetPlistFriendlyName(self.config['app_name']) + if self._GetChromiumVersionString() is not None: + self.WriteComment(self.config['build'] + ''' version: ''' + \ + self._GetChromiumVersionString()) + self._AddToStringTable( + app_name, + self.config['app_name'], + self.messages['mac_chrome_preferences']['text']) + + def Init(self): + # A buffer for the lines of the string table being generated. + self._out = [] + + def GetTemplateText(self): + return '\n'.join(self._out) diff --git a/tools/grit/grit/format/policy_templates/writers/plist_strings_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/plist_strings_writer_unittest.py new file mode 100755 index 0000000..efad6f2 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/plist_strings_writer_unittest.py @@ -0,0 +1,411 @@ +#!/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.policy_templates.writers.plist_strings_writer''' + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest + +from grit.format.policy_templates.writers import writer_unittest_common + + +class PListStringsWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests for PListStringsWriter.''' + + def testEmpty(self): + # Test PListStringsWriter in case of empty polices. + grd = self.PrepareTest(''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': '$1 preferen"ces', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium': '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist_strings', + 'en') + expected_output = ( + 'Chromium.pfm_title = "Chromium";\n' + 'Chromium.pfm_description = "Chromium preferen\\"ces";') + self.assertEquals(output.strip(), expected_output.strip()) + + def testEmptyVersion(self): + # Test PListStringsWriter in case of empty polices. + grd = self.PrepareTest(''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': '$1 preferen"ces', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium': '1', + 'mac_bundle_id': 'com.example.Test', + 'version': '39.0.0.0'}, + 'plist_strings', + 'en') + expected_output = ( + '/* chromium version: 39.0.0.0 */\n' + 'Chromium.pfm_title = "Chromium";\n' + 'Chromium.pfm_description = "Chromium preferen\\"ces";') + self.assertEquals(output.strip(), expected_output.strip()) + + def testMainPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'MainGroup', + 'type': 'group', + 'caption': 'Caption of main.', + 'desc': 'Description of main.', + 'policies': [{ + 'name': 'MainPolicy', + 'type': 'main', + 'supported_on': ['chrome.mac:8-'], + 'caption': 'Caption of main policy.', + 'desc': 'Description of main policy.', + }], + }, + ], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': 'Preferences of $1', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_google_chrome' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist_strings', + 'en') + expected_output = ( + 'Google_Chrome.pfm_title = "Google Chrome";\n' + 'Google_Chrome.pfm_description = "Preferences of Google Chrome";\n' + 'MainPolicy.pfm_title = "Caption of main policy.";\n' + 'MainPolicy.pfm_description = "Description of main policy.";') + self.assertEquals(output.strip(), expected_output.strip()) + + def testStringPolicy(self): + # Tests a policy group with a single policy of type 'string'. Also test + # inheriting group description to policy description. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'StringGroup', + 'type': 'group', + 'caption': 'Caption of group.', + 'desc': """Description of group. +With a newline.""", + 'policies': [{ + 'name': 'StringPolicy', + 'type': 'string', + 'caption': 'Caption of policy.', + 'desc': """Description of policy. +With a newline.""", + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': 'Preferences of $1', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist_strings', + 'en') + expected_output = ( + 'Chromium.pfm_title = "Chromium";\n' + 'Chromium.pfm_description = "Preferences of Chromium";\n' + 'StringPolicy.pfm_title = "Caption of policy.";\n' + 'StringPolicy.pfm_description = ' + '"Description of policy.\\nWith a newline.";') + self.assertEquals(output.strip(), expected_output.strip()) + + def testStringListPolicy(self): + # Tests a policy group with a single policy of type 'list'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'ListGroup', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [{ + 'name': 'ListPolicy', + 'type': 'list', + 'caption': 'Caption of policy.', + 'desc': """Description of policy. +With a newline.""", + 'schema': { + 'type': 'array', + 'items': { 'type': 'string' }, + }, + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': 'Preferences of $1', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist_strings', + 'en') + expected_output = ( + 'Chromium.pfm_title = "Chromium";\n' + 'Chromium.pfm_description = "Preferences of Chromium";\n' + 'ListPolicy.pfm_title = "Caption of policy.";\n' + 'ListPolicy.pfm_description = ' + '"Description of policy.\\nWith a newline.";') + self.assertEquals(output.strip(), expected_output.strip()) + + def testStringEnumListPolicy(self): + # Tests a policy group with a single policy of type 'string-enum-list'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumGroup', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [{ + 'name': 'EnumPolicy', + 'type': 'string-enum-list', + 'caption': 'Caption of policy.', + 'desc': """Description of policy. +With a newline.""", + 'schema': { + 'type': 'array', + 'items': { 'type': 'string' }, + }, + 'items': [ + { + 'name': 'ProxyServerDisabled', + 'value': 'one', + 'caption': 'Option1' + }, + { + 'name': 'ProxyServerAutoDetect', + 'value': 'two', + 'caption': 'Option2' + }, + ], + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': 'Preferences of $1', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist_strings', + 'en') + expected_output = ( + 'Chromium.pfm_title = "Chromium";\n' + 'Chromium.pfm_description = "Preferences of Chromium";\n' + 'EnumPolicy.pfm_title = "Caption of policy.";\n' + 'EnumPolicy.pfm_description = ' + '"one - Option1\\ntwo - Option2\\n' + 'Description of policy.\\nWith a newline.";') + self.assertEquals(output.strip(), expected_output.strip()) + + def testIntEnumPolicy(self): + # Tests a policy group with a single policy of type 'int-enum'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumGroup', + 'type': 'group', + 'desc': '', + 'caption': '', + 'policies': [{ + 'name': 'EnumPolicy', + 'type': 'int-enum', + 'desc': 'Description of policy.', + 'caption': 'Caption of policy.', + 'items': [ + { + 'name': 'ProxyServerDisabled', + 'value': 0, + 'caption': 'Option1' + }, + { + 'name': 'ProxyServerAutoDetect', + 'value': 1, + 'caption': 'Option2' + }, + ], + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': '$1 preferences', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_google_chrome': '1', 'mac_bundle_id': 'com.example.Test2'}, + 'plist_strings', + 'en') + expected_output = ( + 'Google_Chrome.pfm_title = "Google Chrome";\n' + 'Google_Chrome.pfm_description = "Google Chrome preferences";\n' + 'EnumPolicy.pfm_title = "Caption of policy.";\n' + 'EnumPolicy.pfm_description = ' + '"0 - Option1\\n1 - Option2\\nDescription of policy.";\n') + + self.assertEquals(output.strip(), expected_output.strip()) + + def testStringEnumPolicy(self): + # Tests a policy group with a single policy of type 'string-enum'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumGroup', + 'type': 'group', + 'desc': '', + 'caption': '', + 'policies': [{ + 'name': 'EnumPolicy', + 'type': 'string-enum', + 'desc': 'Description of policy.', + 'caption': 'Caption of policy.', + 'items': [ + { + 'name': 'ProxyServerDisabled', + 'value': 'one', + 'caption': 'Option1' + }, + { + 'name': 'ProxyServerAutoDetect', + 'value': 'two', + 'caption': 'Option2' + }, + ], + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': '$1 preferences', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_google_chrome': '1', 'mac_bundle_id': 'com.example.Test2'}, + 'plist_strings', + 'en') + expected_output = ( + 'Google_Chrome.pfm_title = "Google Chrome";\n' + 'Google_Chrome.pfm_description = "Google Chrome preferences";\n' + 'EnumPolicy.pfm_title = "Caption of policy.";\n' + 'EnumPolicy.pfm_description = ' + '"one - Option1\\ntwo - Option2\\nDescription of policy.";\n') + + self.assertEquals(output.strip(), expected_output.strip()) + + def testNonSupportedPolicy(self): + # Tests a policy that is not supported on Mac, so its strings shouldn't + # be included in the plist string table. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'NonMacGroup', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [{ + 'name': 'NonMacPolicy', + 'type': 'string', + 'caption': '', + 'desc': '', + 'supported_on': ['chrome_os:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': { + 'mac_chrome_preferences': { + 'text': '$1 preferences', + 'desc': 'blah' + } + } + }''') + output = self.GetOutput( + grd, + 'fr', + {'_google_chrome': '1', 'mac_bundle_id': 'com.example.Test2'}, + 'plist_strings', + 'en') + expected_output = ( + 'Google_Chrome.pfm_title = "Google Chrome";\n' + 'Google_Chrome.pfm_description = "Google Chrome preferences";') + self.assertEquals(output.strip(), expected_output.strip()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/plist_writer.py b/tools/grit/grit/format/policy_templates/writers/plist_writer.py new file mode 100755 index 0000000..cae6844 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/plist_writer.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. + + +from xml.dom import minidom +from grit.format.policy_templates.writers import plist_helper +from grit.format.policy_templates.writers import xml_formatted_writer + + +# This writer outputs a Preferences Manifest file as documented at +# https://developer.apple.com/library/mac/documentation/MacOSXServer/Conceptual/Preference_Manifest_Files + + +def GetWriter(config): + '''Factory method for creating PListWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return PListWriter(['mac'], config) + + +class PListWriter(xml_formatted_writer.XMLFormattedWriter): + '''Class for generating policy templates in Mac plist format. + It is used by PolicyTemplateGenerator to write plist files. + ''' + + STRING_TABLE = 'Localizable.strings' + TYPE_TO_INPUT = { + 'string': 'string', + 'int': 'integer', + 'int-enum': 'integer', + 'string-enum': 'string', + 'string-enum-list': 'array', + 'main': 'boolean', + 'list': 'array', + 'dict': 'dictionary', + } + + def _AddKeyValuePair(self, parent, key_string, value_tag): + '''Adds a plist key-value pair to a parent XML element. + + A key-value pair in plist consists of two XML elements next two each other: + key_string + ... + + Args: + key_string: The content of the key tag. + value_tag: The name of the value element. + + Returns: + The XML element of the value tag. + ''' + self.AddElement(parent, 'key', {}, key_string) + return self.AddElement(parent, value_tag) + + def _AddStringKeyValuePair(self, parent, key_string, value_string): + '''Adds a plist key-value pair to a parent XML element, where the + value element contains a string. The name of the value element will be + . + + Args: + key_string: The content of the key tag. + value_string: The content of the value tag. + ''' + self.AddElement(parent, 'key', {}, key_string) + self.AddElement(parent, 'string', {}, value_string) + + def _AddTargets(self, parent, policy): + '''Adds the following XML snippet to an XML element: + pfm_targets + + user-managed + + + Args: + parent: The parent XML element where the snippet will be added. + ''' + array = self._AddKeyValuePair(parent, 'pfm_targets', 'array') + if self.CanBeRecommended(policy): + self.AddElement(array, 'string', {}, 'user') + if self.CanBeMandatory(policy): + self.AddElement(array, 'string', {}, 'user-managed') + + def PreprocessPolicies(self, policy_list): + return self.FlattenGroupsAndSortPolicies(policy_list) + + def WritePolicy(self, policy): + policy_name = policy['name'] + policy_type = policy['type'] + if policy_type == 'external': + # This type can only be set through cloud policy. + return + + dict = self.AddElement(self._array, 'dict') + self._AddStringKeyValuePair(dict, 'pfm_name', policy_name) + # Set empty strings for title and description. They will be taken by the + # OSX Workgroup Manager from the string table in a Localizable.strings file. + # Those files are generated by plist_strings_writer. + self._AddStringKeyValuePair(dict, 'pfm_description', '') + self._AddStringKeyValuePair(dict, 'pfm_title', '') + self._AddTargets(dict, policy) + self._AddStringKeyValuePair(dict, 'pfm_type', + self.TYPE_TO_INPUT[policy_type]) + if policy_type in ('int-enum', 'string-enum'): + range_list = self._AddKeyValuePair(dict, 'pfm_range_list', 'array') + for item in policy['items']: + if policy_type == 'int-enum': + element_type = 'integer' + else: + element_type = 'string' + self.AddElement(range_list, element_type, {}, str(item['value'])) + elif policy_type in ('list', 'string-enum-list'): + subkeys = self._AddKeyValuePair(dict, 'pfm_subkeys', 'array') + subkeys_dict = self.AddElement(subkeys, 'dict') + subkeys_type = self._AddKeyValuePair(subkeys_dict, 'pfm_type', 'string') + self.AddText(subkeys_type, 'string') + + def BeginTemplate(self): + self._plist.attributes['version'] = '1' + dict = self.AddElement(self._plist, 'dict') + if self._GetChromiumVersionString() is not None: + self.AddComment(self._plist, self.config['build'] + ' version: ' + \ + self._GetChromiumVersionString()) + app_name = plist_helper.GetPlistFriendlyName(self.config['app_name']) + self._AddStringKeyValuePair(dict, 'pfm_name', app_name) + self._AddStringKeyValuePair(dict, 'pfm_description', '') + self._AddStringKeyValuePair(dict, 'pfm_title', '') + self._AddStringKeyValuePair(dict, 'pfm_version', '1') + self._AddStringKeyValuePair(dict, 'pfm_domain', + self.config['mac_bundle_id']) + + self._array = self._AddKeyValuePair(dict, 'pfm_subkeys', 'array') + + def CreatePlistDocument(self): + dom_impl = minidom.getDOMImplementation('') + doctype = dom_impl.createDocumentType( + 'plist', + '-//Apple//DTD PLIST 1.0//EN', + 'http://www.apple.com/DTDs/PropertyList-1.0.dtd') + return dom_impl.createDocument(None, 'plist', doctype) + + def Init(self): + self._doc = self.CreatePlistDocument() + self._plist = self._doc.documentElement + + def GetTemplateText(self): + return self.ToPrettyXml(self._doc) diff --git a/tools/grit/grit/format/policy_templates/writers/plist_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/plist_writer_unittest.py new file mode 100755 index 0000000..6186c15 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/plist_writer_unittest.py @@ -0,0 +1,691 @@ +#!/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.policy_templates.writers.plist_writer''' + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest + +from grit.format.policy_templates.writers import writer_unittest_common + + +class PListWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests for PListWriter.''' + + def _GetExpectedOutputs(self, product_name, bundle_id, policies): + '''Substitutes the variable parts into a plist template. The result + of this function can be used as an expected result to test the output + of PListWriter. + + Args: + product_name: The name of the product, normally Chromium or Google Chrome. + bundle_id: The mac bundle id of the product. + policies: The list of policies. + + Returns: + The text of a plist template with the variable parts substituted. + ''' + return ''' + + + + + pfm_name + %s + pfm_description + + pfm_title + + pfm_version + 1 + pfm_domain + %s + pfm_subkeys + %s + +''' % (product_name, bundle_id, policies) + + def _GetExpectedOutputsWithVersion(self, product_name, bundle_id, policies, + version): + '''Substitutes the variable parts into a plist template. The result + of this function can be used as an expected result to test the output + of PListWriter. + + Args: + product_name: The name of the product, normally Chromium or Google Chrome. + bundle_id: The mac bundle id of the product. + policies: The list of policies. + + Returns: + The text of a plist template with the variable parts substituted. + ''' + return ''' + + + + + pfm_name + %s + pfm_description + + pfm_title + + pfm_version + 1 + pfm_domain + %s + pfm_subkeys + %s + + +''' % (product_name, bundle_id, policies, version) + + def testEmpty(self): + # Test PListWriter in case of empty polices. + grd = self.PrepareTest(''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': {}, + }''') + + output = self.GetOutput( + grd, + 'fr', + {'_chromium': '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', '') + self.assertEquals(output.strip(), expected_output.strip()) + + def testEmptyVersion(self): + # Test PListWriter in case of empty polices. + grd = self.PrepareTest(''' + { + 'policy_definitions': [], + 'placeholders': [], + 'messages': {}, + }''') + + output = self.GetOutput( + grd, + 'fr', + {'_chromium': '1', + 'mac_bundle_id': 'com.example.Test', + 'version': '39.0.0.0'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputsWithVersion( + 'Chromium', + 'com.example.Test', + '', + 'chromium version: 39.0.0.0') + self.assertEquals(output.strip(), expected_output.strip()) + + def testMainPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'MainGroup', + 'type': 'group', + 'policies': [{ + 'name': 'MainPolicy', + 'type': 'main', + 'desc': '', + 'caption': '', + 'supported_on': ['chrome.mac:8-'], + }], + 'desc': '', + 'caption': '', + }, + ], + 'placeholders': [], + 'messages': {} + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + MainPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + boolean + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testRecommendedPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'MainGroup', + 'type': 'group', + 'policies': [{ + 'name': 'MainPolicy', + 'type': 'main', + 'desc': '', + 'caption': '', + 'features': { + 'can_be_recommended' : True + }, + 'supported_on': ['chrome.mac:8-'], + }], + 'desc': '', + 'caption': '', + }, + ], + 'placeholders': [], + 'messages': {} + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + MainPolicy + pfm_description + + pfm_title + + pfm_targets + + user + user-managed + + pfm_type + boolean + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testRecommendedOnlyPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'MainGroup', + 'type': 'group', + 'policies': [{ + 'name': 'MainPolicy', + 'type': 'main', + 'desc': '', + 'caption': '', + 'features': { + 'can_be_recommended' : True, + 'can_be_mandatory' : False + }, + 'supported_on': ['chrome.mac:8-'], + }], + 'desc': '', + 'caption': '', + }, + ], + 'placeholders': [], + 'messages': {} + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + MainPolicy + pfm_description + + pfm_title + + pfm_targets + + user + + pfm_type + boolean + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testStringPolicy(self): + # Tests a policy group with a single policy of type 'string'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'StringGroup', + 'type': 'group', + 'desc': '', + 'caption': '', + 'policies': [{ + 'name': 'StringPolicy', + 'type': 'string', + 'supported_on': ['chrome.mac:8-'], + 'desc': '', + 'caption': '', + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + StringPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + string + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testListPolicy(self): + # Tests a policy group with a single policy of type 'list'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'ListGroup', + 'type': 'group', + 'desc': '', + 'caption': '', + 'policies': [{ + 'name': 'ListPolicy', + 'type': 'list', + 'schema': { + 'type': 'array', + 'items': { 'type': 'string' }, + }, + 'supported_on': ['chrome.mac:8-'], + 'desc': '', + 'caption': '', + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + ListPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + array + pfm_subkeys + + + pfm_type + string + + + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testStringEnumListPolicy(self): + # Tests a policy group with a single policy of type 'string-enum-list'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'ListGroup', + 'type': 'group', + 'desc': '', + 'caption': '', + 'policies': [{ + 'name': 'ListPolicy', + 'type': 'string-enum-list', + 'schema': { + 'type': 'array', + 'items': { 'type': 'string' }, + }, + 'items': [ + {'name': 'ProxyServerDisabled', 'value': 'one', 'caption': ''}, + {'name': 'ProxyServerAutoDetect', 'value': 'two', 'caption': ''}, + ], + 'supported_on': ['chrome.mac:8-'], + 'supported_on': ['chrome.mac:8-'], + 'desc': '', + 'caption': '', + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + ListPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + array + pfm_subkeys + + + pfm_type + string + + + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testIntPolicy(self): + # Tests a policy group with a single policy of type 'int'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'IntGroup', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [{ + 'name': 'IntPolicy', + 'type': 'int', + 'caption': '', + 'desc': '', + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + IntPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + integer + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testIntEnumPolicy(self): + # Tests a policy group with a single policy of type 'int-enum'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumGroup', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [{ + 'name': 'EnumPolicy', + 'type': 'int-enum', + 'desc': '', + 'caption': '', + 'items': [ + {'name': 'ProxyServerDisabled', 'value': 0, 'caption': ''}, + {'name': 'ProxyServerAutoDetect', 'value': 1, 'caption': ''}, + ], + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_google_chrome': '1', 'mac_bundle_id': 'com.example.Test2'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Google_Chrome', 'com.example.Test2', ''' + + pfm_name + EnumPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + integer + pfm_range_list + + 0 + 1 + + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testStringEnumPolicy(self): + # Tests a policy group with a single policy of type 'string-enum'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'EnumGroup', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [{ + 'name': 'EnumPolicy', + 'type': 'string-enum', + 'desc': '', + 'caption': '', + 'items': [ + {'name': 'ProxyServerDisabled', 'value': 'one', 'caption': ''}, + {'name': 'ProxyServerAutoDetect', 'value': 'two', 'caption': ''}, + ], + 'supported_on': ['chrome.mac:8-'], + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_google_chrome': '1', 'mac_bundle_id': 'com.example.Test2'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Google_Chrome', 'com.example.Test2', ''' + + pfm_name + EnumPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + string + pfm_range_list + + one + two + + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testDictionaryPolicy(self): + # Tests a policy group with a single policy of type 'dict'. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'DictionaryGroup', + 'type': 'group', + 'desc': '', + 'caption': '', + 'policies': [{ + 'name': 'DictionaryPolicy', + 'type': 'dict', + 'supported_on': ['chrome.mac:8-'], + 'desc': '', + 'caption': '', + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_chromium' : '1', 'mac_bundle_id': 'com.example.Test'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Chromium', 'com.example.Test', ''' + + pfm_name + DictionaryPolicy + pfm_description + + pfm_title + + pfm_targets + + user-managed + + pfm_type + dictionary + + ''') + self.assertEquals(output.strip(), expected_output.strip()) + + def testNonSupportedPolicy(self): + # Tests a policy that is not supported on Mac, so it shouldn't + # be included in the plist file. + grd = self.PrepareTest(''' + { + 'policy_definitions': [ + { + 'name': 'NonMacGroup', + 'type': 'group', + 'caption': '', + 'desc': '', + 'policies': [{ + 'name': 'NonMacPolicy', + 'type': 'string', + 'supported_on': ['chrome.linux:8-', 'chrome.win:7-'], + 'caption': '', + 'desc': '', + }], + }, + ], + 'placeholders': [], + 'messages': {}, + }''') + output = self.GetOutput( + grd, + 'fr', + {'_google_chrome': '1', 'mac_bundle_id': 'com.example.Test2'}, + 'plist', + 'en') + expected_output = self._GetExpectedOutputs( + 'Google_Chrome', 'com.example.Test2', '''''') + self.assertEquals(output.strip(), expected_output.strip()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/reg_writer.py b/tools/grit/grit/format/policy_templates/writers/reg_writer.py new file mode 100755 index 0000000..70c87a3 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/reg_writer.py @@ -0,0 +1,117 @@ +#!/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. + + +import json + +from grit.format.policy_templates.writers import template_writer + + +def GetWriter(config): + '''Factory method for creating RegWriter objects. + See the constructor of TemplateWriter for description of + arguments. + ''' + return RegWriter(['win'], config) + + +class RegWriter(template_writer.TemplateWriter): + '''Class for generating policy example files in .reg format (for Windows). + The generated files will define all the supported policies with example + values set for them. This class is used by PolicyTemplateGenerator to + write .reg files. + ''' + + NEWLINE = '\r\n' + + def _EscapeRegString(self, string): + return string.replace('\\', '\\\\').replace('\"', '\\\"') + + def _StartBlock(self, key, suffix, list): + key = 'HKEY_LOCAL_MACHINE\\' + key + if suffix: + key = key + '\\' + suffix + if key != self._last_key.get(id(list), None): + list.append('') + list.append('[%s]' % key) + self._last_key[id(list)] = key + + def PreprocessPolicies(self, policy_list): + return self.FlattenGroupsAndSortPolicies(policy_list, + self.GetPolicySortingKey) + + def GetPolicySortingKey(self, policy): + '''Extracts a sorting key from a policy. These keys can be used for + list.sort() methods to sort policies. + See TemplateWriter.SortPoliciesGroupsFirst for usage. + ''' + is_list = policy['type'] in ('list', 'string-enum-list') + # Lists come after regular policies. + return (is_list, policy['name']) + + def _WritePolicy(self, policy, key, list): + example_value = policy['example_value'] + + if policy['type'] == 'external': + # This type can only be set through cloud policy. + return + elif policy['type'] in ('list', 'string-enum-list'): + self._StartBlock(key, policy['name'], list) + i = 1 + for item in example_value: + escaped_str = self._EscapeRegString(item) + list.append('"%d"="%s"' % (i, escaped_str)) + i = i + 1 + else: + self._StartBlock(key, None, list) + if policy['type'] in ('string', 'string-enum', 'dict'): + example_value_str = json.dumps(example_value, sort_keys=True) + if policy['type'] == 'dict': + example_value_str = '"%s"' % example_value_str + elif policy['type'] == 'main': + if example_value == True: + example_value_str = 'dword:00000001' + else: + example_value_str = 'dword:00000000' + elif policy['type'] in ('int', 'int-enum'): + example_value_str = 'dword:%08x' % example_value + else: + raise Exception('unknown policy type %s:' % policy['type']) + + list.append('"%s"=%s' % (policy['name'], example_value_str)) + + def WriteComment(self, comment): + self._prefix.append('; ' + comment) + + def WritePolicy(self, policy): + if self.CanBeMandatory(policy): + self._WritePolicy(policy, + self.config['win_reg_mandatory_key_name'], + self._mandatory) + + def WriteRecommendedPolicy(self, policy): + self._WritePolicy(policy, + self.config['win_reg_recommended_key_name'], + self._recommended) + + def BeginTemplate(self): + pass + + def EndTemplate(self): + pass + + def Init(self): + self._mandatory = [] + self._recommended = [] + self._last_key = {} + self._prefix = [] + + def GetTemplateText(self): + self._prefix.append('Windows Registry Editor Version 5.00') + if self._GetChromiumVersionString() is not None: + self.WriteComment(self.config['build'] + ' version: ' + \ + self._GetChromiumVersionString()) + all = self._prefix + self._mandatory + self._recommended + return self.NEWLINE.join(all) diff --git a/tools/grit/grit/format/policy_templates/writers/reg_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/reg_writer_unittest.py new file mode 100755 index 0000000..2851a8b --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/reg_writer_unittest.py @@ -0,0 +1,392 @@ +#!/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.policy_templates.writers.reg_writer''' + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest + +from grit.format.policy_templates.writers import writer_unittest_common + + +class RegWriterUnittest(writer_unittest_common.WriterUnittestCommon): + '''Unit tests for RegWriter.''' + + NEWLINE = '\r\n' + + def CompareOutputs(self, output, expected_output): + '''Compares the output of the reg_writer with its expected output. + + Args: + output: The output of the reg writer as returned by grit. + expected_output: The expected output. + + Raises: + AssertionError: if the two strings are not equivalent. + ''' + self.assertEquals( + output.strip(), + expected_output.strip()) + + def testEmpty(self): + # Test the handling of an empty policy list. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [],' + ' "placeholders": [],' + ' "messages": {}' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium': '1', }, 'reg', 'en') + expected_output = 'Windows Registry Editor Version 5.00' + self.CompareOutputs(output, expected_output) + + def testEmptyVersion(self): + # Test the handling of an empty policy list. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [],' + ' "placeholders": [],' + ' "messages": {}' + '}') + output = self.GetOutput( + grd, 'fr', {'_chromium': '1', 'version': '39.0.0.0' }, 'reg', 'en') + expected_output = ('Windows Registry Editor Version 5.00\r\n' + '; chromium version: 39.0.0.0\r\n') + self.CompareOutputs(output, expected_output) + + def testMainPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "MainPolicy",' + ' "type": "main",' + ' "features": { "can_be_recommended": True },' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": True' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome]', + '"MainPolicy"=dword:00000001', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome\\Recommended]', + '"MainPolicy"=dword:00000001']) + self.CompareOutputs(output, expected_output) + + def testRecommendedMainPolicy(self): + # Tests a policy group with a single policy of type 'main'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "MainPolicy",' + ' "type": "main",' + ' "features": {' + ' "can_be_recommended": True,' + ' "can_be_mandatory": False ' + ' },' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": True' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome\\Recommended]', + '"MainPolicy"=dword:00000001']) + self.CompareOutputs(output, expected_output) + + def testStringPolicy(self): + # Tests a policy group with a single policy of type 'string'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "StringPolicy",' + ' "type": "string",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": "hello, world! \\\" \\\\"' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Chromium]', + '"StringPolicy"="hello, world! \\\" \\\\"']) + self.CompareOutputs(output, expected_output) + + def testIntPolicy(self): + # Tests a policy group with a single policy of type 'int'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "IntPolicy",' + ' "type": "int",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": 26' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Chromium]', + '"IntPolicy"=dword:0000001a']) + self.CompareOutputs(output, expected_output) + + def testIntEnumPolicy(self): + # Tests a policy group with a single policy of type 'int-enum'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "EnumPolicy",' + ' "type": "int-enum",' + ' "caption": "",' + ' "desc": "",' + ' "items": [' + ' {"name": "ProxyServerDisabled", "value": 0, "caption": ""},' + ' {"name": "ProxyServerAutoDetect", "value": 1, "caption": ""},' + ' ],' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": 1' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome': '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome]', + '"EnumPolicy"=dword:00000001']) + self.CompareOutputs(output, expected_output) + + def testStringEnumPolicy(self): + # Tests a policy group with a single policy of type 'string-enum'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "EnumPolicy",' + ' "type": "string-enum",' + ' "caption": "",' + ' "desc": "",' + ' "items": [' + ' {"name": "ProxyServerDisabled", "value": "one",' + ' "caption": ""},' + ' {"name": "ProxyServerAutoDetect", "value": "two",' + ' "caption": ""},' + ' ],' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": "two"' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_google_chrome': '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Google\\Chrome]', + '"EnumPolicy"="two"']) + self.CompareOutputs(output, expected_output) + + def testListPolicy(self): + # Tests a policy group with a single policy of type 'list'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "ListPolicy",' + ' "type": "list",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": ["foo", "bar"]' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Chromium\\ListPolicy]', + '"1"="foo"', + '"2"="bar"']) + + def testStringEnumListPolicy(self): + # Tests a policy group with a single policy of type 'string-enum-list'. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "ListPolicy",' + ' "type": "string-enum-list",' + ' "caption": "",' + ' "desc": "",' + ' "items": [' + ' {"name": "ProxyServerDisabled", "value": "foo",' + ' "caption": ""},' + ' {"name": "ProxyServerAutoDetect", "value": "bar",' + ' "caption": ""},' + ' ],' + ' "supported_on": ["chrome.linux:8-"],' + ' "example_value": ["foo", "bar"]' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Chromium\\ListPolicy]', + '"1"="foo"', + '"2"="bar"']) + + def testDictionaryPolicy(self): + # Tests a policy group with a single policy of type 'dict'. + example = { + 'bool': True, + 'dict': { + 'a': 1, + 'b': 2, + }, + 'int': 10, + 'list': [1, 2, 3], + 'string': 'abc', + } + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "DictionaryPolicy",' + ' "type": "dict",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": ' + str(example) + + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Chromium]', + '"DictionaryPolicy"="{"bool": true, "dict": {"a": 1, ' + '"b": 2}, "int": 10, "list": [1, 2, 3], "string": "abc"}"']) + self.CompareOutputs(output, expected_output) + + def testNonSupportedPolicy(self): + # Tests a policy that is not supported on Windows, so it shouldn't + # be included in the .REG file. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "NonWindowsPolicy",' + ' "type": "list",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.mac:8-"],' + ' "example_value": ["a"]' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00']) + self.CompareOutputs(output, expected_output) + + def testPolicyGroup(self): + # Tests a policy group that has more than one policies. + grd = self.PrepareTest( + '{' + ' "policy_definitions": [' + ' {' + ' "name": "Group1",' + ' "type": "group",' + ' "caption": "",' + ' "desc": "",' + ' "policies": [{' + ' "name": "Policy1",' + ' "type": "list",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": ["a", "b"]' + ' },{' + ' "name": "Policy2",' + ' "type": "string",' + ' "caption": "",' + ' "desc": "",' + ' "supported_on": ["chrome.win:8-"],' + ' "example_value": "c"' + ' }],' + ' },' + ' ],' + ' "placeholders": [],' + ' "messages": {},' + '}') + output = self.GetOutput(grd, 'fr', {'_chromium' : '1'}, 'reg', 'en') + expected_output = self.NEWLINE.join([ + 'Windows Registry Editor Version 5.00', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Chromium]', + '"Policy2"="c"', + '', + '[HKEY_LOCAL_MACHINE\\Software\\Policies\\Chromium\\Policy1]', + '"1"="a"', + '"2"="b"']) + self.CompareOutputs(output, expected_output) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/template_writer.py b/tools/grit/grit/format/policy_templates/writers/template_writer.py new file mode 100755 index 0000000..d489d64 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/template_writer.py @@ -0,0 +1,318 @@ +#!/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 TemplateWriter(object): + '''Abstract base class for writing policy templates in various formats. + The methods of this class will be called by PolicyTemplateGenerator. + ''' + + def __init__(self, platforms, config): + '''Initializes a TemplateWriter object. + + Args: + platforms: List of platforms for which this writer can write policies. + config: A dictionary of information required to generate the template. + It contains some key-value pairs, including the following examples: + 'build': 'chrome' or 'chromium' + 'branding': 'Google Chrome' or 'Chromium' + 'mac_bundle_id': The Mac bundle id of Chrome. (Only set when building + for Mac.) + messages: List of all the message strings from the grd file. Most of them + are also present in the policy data structures that are passed to + methods. That is the preferred way of accessing them, this should only + be used in exceptional cases. An example for its use is the + IDS_POLICY_WIN_SUPPORTED_WINXPSP2 message in ADM files, because that + cannot be associated with any policy or group. + ''' + self.platforms = platforms + self.config = config + + def IsDeprecatedPolicySupported(self, policy): + '''Checks if the given deprecated policy is supported by the writer. + + Args: + policy: The dictionary of the policy. + + Returns: + True if the writer chooses to include the deprecated 'policy' in its + output. + ''' + return False + + def IsFuturePolicySupported(self, policy): + '''Checks if the given future policy is supported by the writer. + + Args: + policy: The dictionary of the policy. + + Returns: + True if the writer chooses to include the deprecated 'policy' in its + output. + ''' + return False + + def IsPolicySupported(self, policy): + '''Checks if the given policy is supported by the writer. + In other words, the set of platforms supported by the writer + has a common subset with the set of platforms that support + the policy. + + Args: + policy: The dictionary of the policy. + + Returns: + True if the writer chooses to include 'policy' in its output. + ''' + if ('deprecated' in policy and policy['deprecated'] is True and + not self.IsDeprecatedPolicySupported(policy)): + return False + + if ('future' in policy and policy['future'] is True and + not self.IsFuturePolicySupported(policy)): + return False + + if '*' in self.platforms: + # Currently chrome_os is only catched here. + return True + for supported_on in policy['supported_on']: + for supported_on_platform in supported_on['platforms']: + if supported_on_platform in self.platforms: + return True + return False + + def CanBeRecommended(self, policy): + '''Checks if the given policy can be recommended.''' + return policy.get('features', {}).get('can_be_recommended', False) + + def CanBeMandatory(self, policy): + '''Checks if the given policy can be mandatory.''' + return policy.get('features', {}).get('can_be_mandatory', True) + + def IsPolicySupportedOnPlatform(self, policy, platform): + '''Checks if |policy| is supported on |platform|. + + Args: + policy: The dictionary of the policy. + platform: The platform to check; one of 'win', 'mac', 'linux' or + 'chrome_os'. + ''' + is_supported = lambda x: platform in x['platforms'] + return any(filter(is_supported, policy['supported_on'])) + + def _GetChromiumVersionString(self): + '''Returns the Chromium version string stored in the environment variable + version (if it is set). + + Returns: The Chromium version string or None if it has not been set.''' + + if 'version' in self.config: + return self.config['version'] + + def _GetPoliciesForWriter(self, group): + '''Filters the list of policies in the passed group that are supported by + the writer. + + Args: + group: The dictionary of the policy group. + + Returns: The list of policies of the policy group that are compatible + with the writer. + ''' + if not 'policies' in group: + return [] + result = [] + for policy in group['policies']: + if self.IsPolicySupported(policy): + result.append(policy) + return result + + def Init(self): + '''Initializes the writer. If the WriteTemplate method is overridden, then + this method must be called as first step of each template generation + process. + ''' + pass + + def WriteTemplate(self, template): + '''Writes the given template definition. + + Args: + template: Template definition to write. + + Returns: + Generated output for the passed template definition. + ''' + self.messages = template['messages'] + self.Init() + template['policy_definitions'] = \ + self.PreprocessPolicies(template['policy_definitions']) + self.BeginTemplate() + for policy in template['policy_definitions']: + if policy['type'] == 'group': + child_policies = self._GetPoliciesForWriter(policy) + child_recommended_policies = filter(self.CanBeRecommended, + child_policies) + if child_policies: + # Only write nonempty groups. + self.BeginPolicyGroup(policy) + for child_policy in child_policies: + # Nesting of groups is currently not supported. + self.WritePolicy(child_policy) + self.EndPolicyGroup() + if child_recommended_policies: + self.BeginRecommendedPolicyGroup(policy) + for child_policy in child_recommended_policies: + self.WriteRecommendedPolicy(child_policy) + self.EndRecommendedPolicyGroup() + elif self.IsPolicySupported(policy): + self.WritePolicy(policy) + if self.CanBeRecommended(policy): + self.WriteRecommendedPolicy(policy) + self.EndTemplate() + + return self.GetTemplateText() + + def PreprocessPolicies(self, policy_list): + '''Preprocesses a list of policies according to a given writer's needs. + Preprocessing steps include sorting policies and stripping unneeded + information such as groups (for writers that ignore them). + Subclasses are encouraged to override this method, overriding + implementations may call one of the provided specialized implementations. + The default behaviour is to use SortPoliciesGroupsFirst(). + + Args: + policy_list: A list containing the policies to sort. + + Returns: + The sorted policy list. + ''' + return self.SortPoliciesGroupsFirst(policy_list) + + def WritePolicy(self, policy): + '''Appends the template text corresponding to a policy into the + internal buffer. + + Args: + policy: The policy as it is found in the JSON file. + ''' + raise NotImplementedError() + + def WriteComment(self, comment): + '''Appends the comment to the internal buffer. + + comment: The comment to be added. + ''' + raise NotImplementedError() + + def WriteRecommendedPolicy(self, policy): + '''Appends the template text corresponding to a recommended policy into the + internal buffer. + + Args: + policy: The recommended policy as it is found in the JSON file. + ''' + # TODO + #raise NotImplementedError() + pass + + def BeginPolicyGroup(self, group): + '''Appends the template text corresponding to the beginning of a + policy group into the internal buffer. + + Args: + group: The policy group as it is found in the JSON file. + ''' + pass + + def EndPolicyGroup(self): + '''Appends the template text corresponding to the end of a + policy group into the internal buffer. + ''' + pass + + def BeginRecommendedPolicyGroup(self, group): + '''Appends the template text corresponding to the beginning of a recommended + policy group into the internal buffer. + + Args: + group: The recommended policy group as it is found in the JSON file. + ''' + pass + + def EndRecommendedPolicyGroup(self): + '''Appends the template text corresponding to the end of a recommended + policy group into the internal buffer. + ''' + pass + + def BeginTemplate(self): + '''Appends the text corresponding to the beginning of the whole + template into the internal buffer. + ''' + raise NotImplementedError() + + def EndTemplate(self): + '''Appends the text corresponding to the end of the whole + template into the internal buffer. + ''' + pass + + def GetTemplateText(self): + '''Gets the content of the internal template buffer. + + Returns: + The generated template from the the internal buffer as a string. + ''' + raise NotImplementedError() + + def SortPoliciesGroupsFirst(self, policy_list): + '''Sorts a list of policies alphabetically. The order is the + following: first groups alphabetically by caption, then other policies + alphabetically by name. The order of policies inside groups is unchanged. + + Args: + policy_list: The list of policies to sort. Sub-lists in groups will not + be sorted. + ''' + policy_list.sort(key=self.GetPolicySortingKeyGroupsFirst) + return policy_list + + def FlattenGroupsAndSortPolicies(self, policy_list, sorting_key=None): + '''Sorts a list of policies according to |sorting_key|, defaulting + to alphabetical sorting if no key is given. If |policy_list| contains + policies with type="group", it is flattened first, i.e. any groups' contents + are inserted into the list as first-class elements and the groups are then + removed. + ''' + new_list = [] + for policy in policy_list: + if policy['type'] == 'group': + for grouped_policy in policy['policies']: + new_list.append(grouped_policy) + else: + new_list.append(policy) + if sorting_key == None: + sorting_key = self.GetPolicySortingKeyName + new_list.sort(key=sorting_key) + return new_list + + def GetPolicySortingKeyName(self, policy): + return policy['name'] + + def GetPolicySortingKeyGroupsFirst(self, policy): + '''Extracts a sorting key from a policy. These keys can be used for + list.sort() methods to sort policies. + See TemplateWriter.SortPolicies for usage. + ''' + is_group = policy['type'] == 'group' + if is_group: + # Groups are sorted by caption. + str_key = policy['caption'] + else: + # Regular policies are sorted by name. + str_key = policy['name'] + # Groups come before regular policies. + return (not is_group, str_key) diff --git a/tools/grit/grit/format/policy_templates/writers/template_writer_unittest.py b/tools/grit/grit/format/policy_templates/writers/template_writer_unittest.py new file mode 100755 index 0000000..172e292 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/template_writer_unittest.py @@ -0,0 +1,84 @@ +#!/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.policy_templates.writers.template_writer''' + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../../../..')) + +import unittest + +from grit.format.policy_templates.writers import template_writer + + +POLICY_DEFS = [ + {'name': 'zp', 'type': 'string', 'caption': 'a1', 'supported_on': []}, + { + 'type': 'group', + 'caption': 'z_group1_caption', + 'name': 'group1', + 'policies': [ + {'name': 'z0', 'type': 'string', 'supported_on': []}, + {'name': 'a0', 'type': 'string', 'supported_on': []} + ] + }, + { + 'type': 'group', + 'caption': 'b_group2_caption', + 'name': 'group2', + 'policies': [{'name': 'q', 'type': 'string', 'supported_on': []}], + }, + {'name': 'ap', 'type': 'string', 'caption': 'a2', 'supported_on': []} +] + + +GROUP_FIRST_SORTED_POLICY_DEFS = [ + { + 'type': 'group', + 'caption': 'b_group2_caption', + 'name': 'group2', + 'policies': [{'name': 'q', 'type': 'string', 'supported_on': []}], + }, + { + 'type': 'group', + 'caption': 'z_group1_caption', + 'name': 'group1', + 'policies': [ + {'name': 'z0', 'type': 'string', 'supported_on': []}, + {'name': 'a0', 'type': 'string', 'supported_on': []} + ] + }, + {'name': 'ap', 'type': 'string', 'caption': 'a2', 'supported_on': []}, + {'name': 'zp', 'type': 'string', 'caption': 'a1', 'supported_on': []}, +] + + +IGNORE_GROUPS_SORTED_POLICY_DEFS = [ + {'name': 'a0', 'type': 'string', 'supported_on': []}, + {'name': 'ap', 'type': 'string', 'caption': 'a2', 'supported_on': []}, + {'name': 'q', 'type': 'string', 'supported_on': []}, + {'name': 'z0', 'type': 'string', 'supported_on': []}, + {'name': 'zp', 'type': 'string', 'caption': 'a1', 'supported_on': []}, +] + + +class TemplateWriterUnittests(unittest.TestCase): + '''Unit tests for templater_writer.py.''' + + def testSortingGroupsFirst(self): + tw = template_writer.TemplateWriter(None, None) + sorted_list = tw.SortPoliciesGroupsFirst(POLICY_DEFS) + self.assertEqual(sorted_list, GROUP_FIRST_SORTED_POLICY_DEFS) + + def testSortingIgnoreGroups(self): + tw = template_writer.TemplateWriter(None, None) + sorted_list = tw.FlattenGroupsAndSortPolicies(POLICY_DEFS) + self.assertEqual(sorted_list, IGNORE_GROUPS_SORTED_POLICY_DEFS) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/format/policy_templates/writers/writer_unittest_common.py b/tools/grit/grit/format/policy_templates/writers/writer_unittest_common.py new file mode 100755 index 0000000..f75c391 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/writer_unittest_common.py @@ -0,0 +1,83 @@ +#!/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. + +'''Common tools for unit-testing writers.''' + + +import os +import tempfile +import unittest +import StringIO + +from grit import grd_reader +from grit import util +from grit.tool import build + + +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 WriterUnittestCommon(unittest.TestCase): + '''Common class for unittesting writers.''' + + def PrepareTest(self, policy_json): + '''Prepares and parses a grit tree along with a data structure of policies. + + Args: + policy_json: The policy data structure in JSON format. + ''' + # First create a temporary file that contains the JSON policy list. + tmp_file_name = 'test.json' + tmp_dir_name = tempfile.gettempdir() + json_file_path = tmp_dir_name + '/' + tmp_file_name + with open(json_file_path, 'w') as f: + f.write(policy_json.strip()) + # Then assemble the grit tree. + grd_text = ''' + + + + + + + ''' % json_file_path + grd_string_io = StringIO.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) + grd.SetOutputLanguage('en') + grd.RunGatherers() + # Remove the policies' JSON. + os.unlink(json_file_path) + return grd + + def GetOutput(self, grd, env_lang, env_defs, out_type, out_lang): + '''Generates an output of a writer. + + Args: + grd: The root of the grit tree. + env_lang: The environment language. + env_defs: Environment definitions. + out_type: Type of the output node for which output will be generated. + This selects the writer. + out_lang: Language of the output node for which output will be generated. + + Returns: + The string of the template created by the writer. + ''' + grd.SetOutputLanguage(env_lang) + grd.SetDefines(env_defs) + buf = StringIO.StringIO() + build.RcBuilder.ProcessNode(grd, DummyOutput(out_type, out_lang), buf) + return buf.getvalue() diff --git a/tools/grit/grit/format/policy_templates/writers/xml_formatted_writer.py b/tools/grit/grit/format/policy_templates/writers/xml_formatted_writer.py new file mode 100755 index 0000000..5917fb4 --- /dev/null +++ b/tools/grit/grit/format/policy_templates/writers/xml_formatted_writer.py @@ -0,0 +1,91 @@ +#!/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. + + +from grit.format.policy_templates.writers import template_writer + + +class XMLFormattedWriter(template_writer.TemplateWriter): + '''Helper class for generating XML-based templates. + ''' + + def AddElement(self, parent, name, attrs=None, text=None): + ''' + Adds a new XML Element as a child to an existing element or the Document. + + Args: + parent: An XML element or the document, where the new element will be + added. + name: The name of the new element. + attrs: A dictionary of the attributes' names and values for the new + element. + text: Text content for the new element. + + Returns: + The created new element. + ''' + if attrs == None: + attrs = {} + + doc = parent.ownerDocument + element = doc.createElement(name) + for key, value in sorted(attrs.iteritems()): + element.setAttribute(key, value) + if text: + element.appendChild(doc.createTextNode(text)) + parent.appendChild(element) + return element + + def AddText(self, parent, text): + '''Adds text to a parent node. + ''' + doc = parent.ownerDocument + parent.appendChild(doc.createTextNode(text)) + + def AddAttribute(self, parent, name, value): + '''Adds a new attribute to the parent Element. If an attribute with the + given name already exists then it will be replaced. + ''' + doc = parent.ownerDocument + attribute = doc.createAttribute(name) + attribute.value = value + parent.setAttributeNode(attribute) + + def AddComment(self, parent, comment): + '''Adds a comment node.''' + parent.appendChild(parent.ownerDocument.createComment(comment)) + + def ToPrettyXml(self, doc, **kwargs): + # return doc.toprettyxml(indent=' ') + # The above pretty-printer does not print the doctype and adds spaces + # around texts, e.g.: + # + # value of the string + # + # This is problematic both for the OSX Workgroup Manager (plist files) and + # the Windows Group Policy Editor (admx files). What they need instead: + # value of string + # So we use the poor man's pretty printer here. It assumes that there are + # no mixed-content nodes. + # Get all the XML content in a one-line string. + xml = doc.toxml(**kwargs) + # Determine where the line breaks will be. (They will only be between tags.) + lines = xml[1:len(xml) - 1].split('><') + indent = '' + res = '' + # Determine indent for each line. + for i, line in enumerate(lines): + if line[0] == '/': + # If the current line starts with a closing tag, decrease indent before + # printing. + indent = indent[2:] + lines[i] = indent + '<' + line + '>' + if (line[0] not in ['/', '?', '!'] and '\n\s+([^<>\s].*?)\n\s*\g<1> 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_CHARSET_PAIR.has_key(language): + return _LANGUAGE_CHARSET_PAIR[language] + elif language == 'no-specific-language': + return '' + else: + print 'Warning:GetLangCharsetPair() found undefined language %s' %(language) + return '' + +def GetLangDirectivePair(language): + if _LANGUAGE_DIRECTIVE_PAIR.has_key(language): + return _LANGUAGE_DIRECTIVE_PAIR[language] + else: + # 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_CHARSET_PAIR.has_key(language): + langcharset = _LANGUAGE_CHARSET_PAIR[language] + lang_id = '0x' + langcharset[0:4] + return lang_id + elif language == 'no-specific-language': + return '' + else: + print 'Warning:GetLangIdHex() found undefined language %s' %(language) + return '' + + +def GetCharsetIdDecimal(language): + if _LANGUAGE_CHARSET_PAIR.has_key(language): + langcharset = _LANGUAGE_CHARSET_PAIR[language] + charset_decimal = int(langcharset[4:], 16) + return str(charset_decimal) + elif language == 'no-specific-language': + return '' + else: + 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() + else : + 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, types.StringTypes) + 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 +#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, types.StringTypes) + from grit.node import structure + assert isinstance(item, structure.StructureNode) + + if item.IsExcludedFromRc(): + return '' + else: + 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, types.StringTypes) + 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', 'igoogle', + 'muppet', '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 '' + else: + return '%-18s %-18s "%s"\n' % (item.attrs['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'), + 'igoogle' : partial(FormatInclude, type='XML'), + 'muppet' : partial(FormatInclude, type='XML'), + '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 100755 index 0000000..74e7127 --- /dev/null +++ b/tools/grit/grit/format/rc_header.py @@ -0,0 +1,204 @@ +#!/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. + +'''Item formatters for RC headers. +''' + +from grit import exception +from grit import util +from grit.extern import FP + + +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 ', ''] + 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' + + for line in FormatDefines(root, root.ShouldOutputAllResourceDefines(), + root.GetRcHeaderFormat()): + yield line + + +def FormatDefines(root, output_all_resource_defines=True, + rc_header_format=None): + '''Yields #define SYMBOL 1234 lines. + + Args: + root: A GritNode. + output_all_resource_defines: If False, output only the symbols used in the + current output configuration. + ''' + from grit.node import message + tids = GetIds(root) + + if output_all_resource_defines: + items = root.Preorder() + else: + items = root.ActiveDescendants() + + if not rc_header_format: + rc_header_format = "#define {textual_id} {numeric_id}" + rc_header_format += "\n" + seen = set() + for item in items: + if not isinstance(item, message.MessageNode): + with item: + for tid in item.GetTextualIds(): + if tid in tids and tid not in seen: + seen.add(tid) + yield rc_header_format.format(textual_id=tid,numeric_id=tids[tid]) + + # Temporarily mimic old behavior: MessageNodes were only output if active, + # even with output_all_resource_defines set. TODO(benrg): Remove this after + # fixing problems in the Chrome tree. + for item in root.ActiveDescendants(): + if isinstance(item, message.MessageNode): + with item: + for tid in item.GetTextualIds(): + if tid in tids and tid not in seen: + seen.add(tid) + yield rc_header_format.format(textual_id=tid,numeric_id=tids[tid]) + + +_cached_ids = {} + + +def GetIds(root): + '''Return a dictionary mapping textual ids to numeric ids for the given tree. + + Args: + root: A GritNode. + ''' + # TODO(benrg): Since other formatters use this, it might make sense to move it + # and _ComputeIds to GritNode and store the cached ids as an attribute. On the + # other hand, GritNode has too much random stuff already. + if root not in _cached_ids: + _cached_ids[root] = _ComputeIds(root) + return _cached_ids[root] + + +def _ComputeIds(root): + from grit.node import empty, include, message, misc, structure + + ids = {} # Maps numeric id to textual id + tids = {} # Maps textual id to numeric id + id_reasons = {} # Maps numeric id to text id and a human-readable explanation + group = None + last_id = None + + for item in root: + if isinstance(item, empty.GroupingNode): + # Note: this won't work if any GroupingNode can be contained inside + # another. + group = item + last_id = None + continue + + assert not item.GetTextualIds() or isinstance(item, + (include.IncludeNode, message.MessageNode, + misc.IdentifierNode, structure.StructureNode)) + + # Resources that use the RES protocol don't need + # any numerical ids generated, so we skip them altogether. + # This is accomplished by setting the flag 'generateid' to false + # in the GRD file. + if item.attrs.get('generateid', 'true') == 'false': + continue + + for tid in item.GetTextualIds(): + if util.SYSTEM_IDENTIFIERS.match(tid): + # Don't emit a new ID for predefined IDs + continue + + if tid in tids: + continue + + # Some identifier nodes can provide their own id, + # and we use that id in the generated header in that case. + if hasattr(item, 'GetId') and item.GetId(): + id = long(item.GetId()) + reason = 'returned by GetId() method' + + elif ('offset' in item.attrs and group and + group.attrs.get('first_id', '') != ''): + offset_text = item.attrs['offset'] + parent_text = group.attrs['first_id'] + + try: + offset_id = long(offset_text) + except ValueError: + offset_id = tids[offset_text] + + try: + parent_id = long(parent_text) + except ValueError: + parent_id = tids[parent_text] + + id = parent_id + offset_id + reason = 'first_id %d + offset %d' % (parent_id, offset_id) + + # We try to allocate IDs sequentially for blocks of items that might + # be related, for instance strings in a stringtable (as their IDs might be + # used e.g. as IDs for some radio buttons, in which case the IDs must + # be sequential). + # + # We do this by having the first item in a section store its computed ID + # (computed from a fingerprint) in its parent object. Subsequent children + # of the same parent will then try to get IDs that sequentially follow + # the currently stored ID (on the parent) and increment it. + elif last_id is None: + # First check if the starting ID is explicitly specified by the parent. + if group and group.attrs.get('first_id', '') != '': + id = long(group.attrs['first_id']) + reason = "from parent's first_id attribute" + else: + # Automatically generate the ID based on the first clique from the + # first child of the first child node of our parent (i.e. when we + # first get to this location in the code). + + # According to + # http://msdn.microsoft.com/en-us/library/t2zechd4(VS.71).aspx + # the safe usable range for resource IDs in Windows is from decimal + # 101 to 0x7FFF. + + id = FP.UnsignedFingerPrint(tid) + id = id % (0x7FFF - 101) + 101 + reason = 'chosen by random fingerprint -- use first_id to override' + + last_id = id + else: + id = last_id = last_id + 1 + reason = 'sequentially assigned' + + reason = "%s (%s)" % (tid, reason) + # Don't fail when 'offset' is specified, as the base and the 0th + # offset will have the same ID. + if id in id_reasons and not 'offset' in item.attrs: + raise exception.IdRangeOverlap('ID %d was assigned to both %s and %s.' + % (id, id_reasons[id], reason)) + + if id < 101: + print ('WARNING: Numeric resource IDs should be greater than 100 to\n' + 'avoid conflicts with system-defined resource IDs.') + + ids[id] = tid + tids[tid] = id + id_reasons[id] = reason + + return tids diff --git a/tools/grit/grit/format/rc_header_unittest.py b/tools/grit/grit/format/rc_header_unittest.py new file mode 100755 index 0000000..5d780e3 --- /dev/null +++ b/tools/grit/grit/format/rc_header_unittest.py @@ -0,0 +1,193 @@ +#!/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 + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import StringIO +import unittest + +from grit import exception +from grit import grd_reader +from grit import util +from grit.format import rc_header + + +class RcHeaderFormatterUnittest(unittest.TestCase): + def FormatAll(self, grd): + output = rc_header.FormatDefines(grd, grd.ShouldOutputAllResourceDefines()) + return ''.join(output).replace(' ', '') + + def testFormatter(self): + grd = grd_reader.Parse(StringIO.StringIO(''' + + + + + + + + Hello %sJoi, how are you doing today? + + + Bongo! + + + + + + + + '''), '.') + output = self.FormatAll(grd) + self.failUnless(output.count('IDS_GREETING10000')) + self.failUnless(output.count('ID_LOGO300')) + + def testOnlyDefineResourcesThatSatisfyOutputCondition(self): + grd = grd_reader.Parse(StringIO.StringIO(''' + + + + + + + + I will appear in the .rc file. + + + + I will not appear in the .rc file. + + + + + Hello. + + + + + Hola. + + + + I will also appear in the .rc file. + + + + '''), '.') + output = self.FormatAll(grd) + self.failUnless(output.count('IDS_FIRSTPRESENTSTRING10000')) + self.failIf(output.count('IDS_MISSINGSTRING')) + self.failIf(output.count('10001')) # IDS_MISSINGSTRING should get this ID + self.failUnless(output.count('IDS_LANGUAGESPECIFICSTRING10002')) + self.failUnless(output.count('IDS_THIRDPRESENTSTRING10003')) + + def testExplicitFirstIdOverlaps(self): + # second first_id will overlap preexisting range + grd = grd_reader.Parse(StringIO.StringIO(''' + + + + + + + + + Hello %sJoi, how are you doing today? + + Frubegfrums + + + '''), '.') + self.assertRaises(exception.IdRangeOverlap, self.FormatAll, grd) + + def testImplicitOverlapsPreexisting(self): + # second message in will overlap preexisting range + grd = grd_reader.Parse(StringIO.StringIO(''' + + + + + + + + + Hello %sJoi, how are you doing today? + + Frubegfrums + + + '''), '.') + self.assertRaises(exception.IdRangeOverlap, self.FormatAll, grd) + + def testEmit(self): + grd = grd_reader.Parse(StringIO.StringIO(''' + + + + Wrong + + + + No + + + + Error + + + Bingo + + + '''), '.') + output = ''.join(rc_header.Format(grd, 'en', '.')) + output = util.StripBlankLinesAndComments(output) + self.assertEqual('#pragma once\nBingo', output) + + def testRcHeaderFormat(self): + grd = grd_reader.Parse(StringIO.StringIO(''' + + + + + + + + Hello %sJoi, how are you doing today? + + + Bongo! + + + + '''), '.') + + # Using the default rc_header format string. + output = rc_header.FormatDefines(grd, grd.ShouldOutputAllResourceDefines(), + grd.GetRcHeaderFormat()) + self.assertEqual(('#define IDR_LOGO 300\n' + '#define IDS_GREETING 10000\n' + '#define IDS_BONGO 10001\n'), ''.join(output)) + + # Using a custom rc_header format string. + grd.AssignRcHeaderFormat( + '#define {textual_id} _Pragma("{textual_id}") {numeric_id}') + output = rc_header.FormatDefines(grd, grd.ShouldOutputAllResourceDefines(), + grd.GetRcHeaderFormat()) + self.assertEqual(('#define IDR_LOGO _Pragma("IDR_LOGO") 300\n' + '#define IDS_GREETING _Pragma("IDS_GREETING") 10000\n' + '#define IDS_BONGO _Pragma("IDS_BONGO") 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 100755 index 0000000..a38001b --- /dev/null +++ b/tools/grit/grit/format/rc_unittest.py @@ -0,0 +1,409 @@ +#!/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''' + +import os +import re +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import tempfile +import unittest +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 +#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(''' + + Go! + + Hello %sJoi, how are you doing today? + + + Howdie "Mr. Elephant", how are you doing? \'\'\' + + +Good day sir, +I am a bee +Sting sting + + + ''') + + buf = StringIO.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(''' + + + + + ''') + root.SetOutputLanguage('en') + root.RunGatherers() + + buf = StringIO.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(''' + + + + ''', base_dir = '/temp') + # We do not run gatherers as it is not needed and wouldn't find the file + + buf = StringIO.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('"[c-zC-Z]:', '"', output) + self.assertEqual(expected, output) + + def testRcIncludeFile(self): + root = util.ParseGrdForUnittest(''' + + + + ''', base_dir = '/temp') + + buf = StringIO.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('"[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(''' + + + ''' % input_file) + + buf = StringIO.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('"[c-zC-Z]:', '"', output) + self.assertEqual(expected, output) + + file_contents = util.ReadFile(output_file, util.RAW_TEXT) + + # Check for the content added by the 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 "" and "" tags. + self.failUnless(file_contents.find('') == -1) + + + def testStructureNodeOutputfile(self): + input_file = util.PathFromRoot('grit/testdata/simple.html') + root = util.ParseGrdForUnittest('''\ + + + ''' % 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, util.RAW_TEXT) + + self.failUnless(contents.find('

') != -1) # should contain the markup + self.failUnless(contents.find('Hello!') == -1) # should be translated + + + 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('''\ + + + ''' % 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.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('"[c-zC-Z]:', '"', output) + self.assertEqual(expected, output) + + file_contents = util.ReadFile(output_file, util.RAW_TEXT) + + # Check for the content added by the 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) + + + def testSubstitutionHtml(self): + input_file = util.PathFromRoot('grit/testdata/toolbar_about.html') + root = grd_reader.Parse(StringIO.StringIO(''' + + + + + + + + ''' % 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, util.RAW_TEXT) + + self.failUnless(contents.find('dir="RTL"') != -1) + + + def testFallbackToEnglish(self): + root = util.ParseGrdForUnittest('''\ + + + ''', base_dir=util.PathFromRoot('.')) + root.SetOutputLanguage('en') + root.RunGatherers() + + buf = StringIO.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.StringIO(''' + + + + + + + + + + + excellent + + + + + '''), util.PathFromRoot('.')) + root.SetOutputLanguage('en') + root.RunGatherers() + + buf = StringIO.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 +#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/repack.py b/tools/grit/grit/format/repack.py new file mode 100755 index 0000000..337b7af --- /dev/null +++ b/tools/grit/grit/format/repack.py @@ -0,0 +1,37 @@ +#!/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. + +""" +A simple utility function to merge data pack files into a single data pack. See +http://dev.chromium.org/developers/design-documents/linuxresourcesandlocalizedstrings +for details about the file format. +""" + +import optparse +import os +import sys + +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import grit.format.data_pack + + +def main(argv): + parser = optparse.OptionParser('usage: %prog [options] ' + ' [input_file2] ...') + parser.add_option('--whitelist', action='store', dest='whitelist', + default=None, help='Full path to the whitelist used to' + 'filter output pak file resource IDs') + options, file_paths = parser.parse_args(argv) + + if len(file_paths) < 2: + parser.error('Please specify output and at least one input filenames') + + grit.format.data_pack.RePack(file_paths[0], file_paths[1:], + whitelist_file=options.whitelist) + +if '__main__' == __name__: + main(sys.argv[1:]) diff --git a/tools/grit/grit/format/resource_map.py b/tools/grit/grit/format/resource_map.py new file mode 100755 index 0000000..37ac54a --- /dev/null +++ b/tools/grit/grit/format/resource_map.py @@ -0,0 +1,132 @@ +#!/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. + +'''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.''' + +import os +from functools import partial + +from grit import util + + +def GetFormatter(type): + if type == 'resource_map_header': + return _FormatHeader + elif type == 'resource_map_source': + return partial(_FormatSource, _GetItemName) + elif type == 'resource_file_map_source': + return partial(_FormatSource, _GetItemPath) + + +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 filename.find('_') != -1: + pos = filename.find('_') + if 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 + +#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): + '''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(): + if 'rc_header' == output.GetType(): + rc_header_file = output.GetFilename() + elif 'resource_map_header' == output.GetType(): + map_header_file = output.GetFilename() + if not rc_header_file or not map_header_file: + raise Exception('resource_map_source output type requires ' + 'resource_map_header and rc_header outputs') + return '''\ +// This file is automatically generated by GRIT. Do not edit. + +#include "%(map_header_file)s" + +#include "base/basictypes.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 = arraysize(%(map_name)s); +''' % { 'map_name': GetMapName(root) } + + +def _FormatSource(get_key, root, lang, output_dir): + from grit.format import rc_header + from grit.node import include, structure, message + yield _FormatSourceHeader(root) + tids = rc_header.GetIds(root) + seen = set() + active_descendants = [item for item in root.ActiveDescendants()] + output_all_resource_defines = root.ShouldOutputAllResourceDefines() + for item in root: + if not item.IsResourceMapSource(): + continue + key = get_key(item) + tid = item.attrs['name'] + if tid not in tids or key in seen: + continue + seen.add(key) + if item.GeneratesResourceMapEntry(output_all_resource_defines, + item in active_descendants): + yield ' {"%s", %s},\n' % (key, tid) + yield _FormatSourceFooter(root) + + +def _GetItemName(item): + return item.attrs['name'] + + +def _GetItemPath(item): + return item.GetInputPath().replace("\\", "/") diff --git a/tools/grit/grit/format/resource_map_unittest.py b/tools/grit/grit/format/resource_map_unittest.py new file mode 100755 index 0000000..55de504 --- /dev/null +++ b/tools/grit/grit/format/resource_map_unittest.py @@ -0,0 +1,279 @@ +#!/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''' + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import StringIO +import unittest + +from grit import grd_reader +from grit import util +from grit.format import resource_map + + +class FormatResourceMapUnittest(unittest.TestCase): + def testFormatResourceMap(self): + grd = grd_reader.Parse(StringIO.StringIO( + ''' + + + + + + + + + + + + + + + + + + + + + + + + '''), util.PathFromRoot('.')) + grd.SetOutputLanguage('en') + grd.RunGatherers() + output = util.StripBlankLinesAndComments(''.join( + resource_map.GetFormatter('resource_map_header')(grd, 'en', '.'))) + self.assertEqual('''\ +#include +#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 "base/basictypes.h" +#include "the_rc_header.h" +const GritResourceMap kTheRcHeader[] = { + {"IDC_KLONKMENU", IDC_KLONKMENU}, + {"IDS_FIRSTPRESENT", IDS_FIRSTPRESENT}, + {"IDS_MISSING", IDS_MISSING}, + {"IDS_LANGUAGESPECIFIC", IDS_LANGUAGESPECIFIC}, + {"IDS_THIRDPRESENT", IDS_THIRDPRESENT}, +}; +const size_t kTheRcHeaderSize = arraysize(kTheRcHeader);''', output) + output = util.StripBlankLinesAndComments(''.join( + resource_map.GetFormatter('resource_file_map_source')(grd, 'en', '.'))) + self.assertEqual('''\ +#include "the_resource_map_header.h" +#include "base/basictypes.h" +#include "the_rc_header.h" +const GritResourceMap kTheRcHeader[] = { + {"grit/testdata/klonk.rc", IDC_KLONKMENU}, + {"abc", IDS_FIRSTPRESENT}, + {"def", IDS_MISSING}, + {"ghi", IDS_LANGUAGESPECIFIC}, + {"jkl", IDS_LANGUAGESPECIFIC}, + {"mno", IDS_THIRDPRESENT}, +}; +const size_t kTheRcHeaderSize = arraysize(kTheRcHeader);''', output) + + def testFormatResourceMapWithOutputAllEqualsFalseForStructures(self): + grd = grd_reader.Parse(StringIO.StringIO( + ''' + + + + + + + + + + + + + + + '''), util.PathFromRoot('.')) + grd.SetOutputLanguage('en') + grd.RunGatherers() + output = util.StripBlankLinesAndComments(''.join( + resource_map.GetFormatter('resource_map_header')(grd, 'en', '.'))) + self.assertEqual('''\ +#include +#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 "base/basictypes.h" +#include "the_rc_header.h" +const GritResourceMap kTheRcHeader[] = { + {"IDR_KLONKMENU", IDR_KLONKMENU}, +}; +const size_t kTheRcHeaderSize = arraysize(kTheRcHeader);''', output) + output = util.StripBlankLinesAndComments(''.join( + resource_map.GetFormatter('resource_map_source')(grd, 'en', '.'))) + self.assertEqual('''\ +#include "the_resource_map_header.h" +#include "base/basictypes.h" +#include "the_rc_header.h" +const GritResourceMap kTheRcHeader[] = { + {"IDR_KLONKMENU", IDR_KLONKMENU}, +}; +const size_t kTheRcHeaderSize = arraysize(kTheRcHeader);''', output) + + def testFormatResourceMapWithOutputAllEqualsFalseForIncludes(self): + grd = grd_reader.Parse(StringIO.StringIO( + ''' + + + + + + + + + + + + + + + + + + '''), util.PathFromRoot('.')) + grd.SetOutputLanguage('en') + grd.RunGatherers() + output = util.StripBlankLinesAndComments(''.join( + resource_map.GetFormatter('resource_map_header')(grd, 'en', '.'))) + self.assertEqual('''\ +#include +#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 "base/basictypes.h" +#include "the_rc_header.h" +const GritResourceMap kTheRcHeader[] = { + {"IDC_KLONKMENU", IDC_KLONKMENU}, + {"IDS_FIRSTPRESENT", IDS_FIRSTPRESENT}, + {"IDS_THIRDPRESENT", IDS_THIRDPRESENT}, +}; +const size_t kTheRcHeaderSize = arraysize(kTheRcHeader);''', output) + output = util.StripBlankLinesAndComments(''.join( + resource_map.GetFormatter('resource_file_map_source')(grd, 'en', '.'))) + self.assertEqual('''\ +#include "the_resource_map_header.h" +#include "base/basictypes.h" +#include "the_rc_header.h" +const GritResourceMap kTheRcHeader[] = { + {"grit/testdata/klonk.rc", IDC_KLONKMENU}, + {"abc", IDS_FIRSTPRESENT}, + {"mno", IDS_THIRDPRESENT}, +}; +const size_t kTheRcHeaderSize = arraysize(kTheRcHeader);''', output) + + def testFormatStringResourceMap(self): + grd = grd_reader.Parse(StringIO.StringIO( + ''' + + + + + + + + + + Application + + + + New Tab + + + + + New tab + + + + + '''), util.PathFromRoot('.')) + grd.SetOutputLanguage('en') + grd.RunGatherers() + output = util.StripBlankLinesAndComments(''.join( + resource_map.GetFormatter('resource_map_header')(grd, 'en', '.'))) + self.assertEqual('''\ +#include +#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 "base/basictypes.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 = arraysize(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 100755 index 0000000..e52734a --- /dev/null +++ b/tools/grit/grit/gather/__init__.py @@ -0,0 +1,9 @@ +#!/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. + +'''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 100755 index 0000000..edf783b --- /dev/null +++ b/tools/grit/grit/gather/admin_template.py @@ -0,0 +1,61 @@ +#!/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. + +'''Gatherer for administrative template files. +''' + +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( + '(?P.+^\[strings\])(?P.+)\Z', + re.MULTILINE | re.DOTALL) + + # Finds the translateable sections from within the [strings] section. + _TRANSLATEABLES = lazy_re.compile( + '^\s*[A-Za-z0-9_]+\s*=\s*"(?P.+)"\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 100755 index 0000000..6c7e56b --- /dev/null +++ b/tools/grit/grit/gather/admin_template_unittest.py @@ -0,0 +1,117 @@ +#!/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.''' + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import StringIO +import tempfile +import unittest + +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.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.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.StringIO(''' + + + + + + + + + + + '''), 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 = tempfile.mkdtemp() + try: + tool = build.RcBuilder() + tool.o = grit_runner.Options() + tool.output_directory = dirname + tool.res = grd + tool.Process() + + self.failUnless(os.path.isfile( + os.path.join(dirname, 'de_GoogleDesktop.adm'))) + self.failUnless(os.path.isfile( + os.path.join(dirname, 'de_README.txt'))) + finally: + for f in os.listdir(dirname): + os.unlink(os.path.join(dirname, f)) + os.rmdir(dirname) + +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 100755 index 0000000..e7469bf --- /dev/null +++ b/tools/grit/grit/gather/chrome_html.py @@ -0,0 +1,360 @@ +#!/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. + +"""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. +""" + +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( + '(?Pchrome://theme/IDR_[A-Z0-9_]*)(?P\?.*)?') +# Pattern for matching CSS url() function. +_CSS_URL_PATTERN = 'url\((?P"|\'|)(?P[^"\'()]*)(?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( + '(?Pcontent|background|[\w-]*-image):\s*' + + '(?P(' + _CSS_URL_PATTERN + '\s*,?\s*)+)') +# Matches CSS image sets. +_CSS_IMAGE_SETS = lazy_re.compile( + '(?Pcontent|background|[\w-]*-image):[ ]*' + + '-webkit-image-set\((?P' + + '(\s*,?\s*url\((?P"|\'|)[^"\'()]*(?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('\s*,?\s*' + + 'url\((?P"|\'|)[^"\'()]*(?P=quote)\)[ ]*(?P[0-9.]*x)', + re.MULTILINE) +_HTML_IMAGE_SRC = lazy_re.compile( + ']+src=\"(?P[^">]*)\"[^>]*>') + +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. + scale_image_name = re.sub('(?P(.*/)?)(?P[^/]*)', + '\\g' + scale_factor + '/\\g', + filename) + 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 . + + 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 . + 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)).decode('utf-8').encode('utf-8') + + +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 + # 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.flatten_html_ = ('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.""" + return self.inlined_text_ + + 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()), + 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 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_, + 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 100755 index 0000000..9b17c11 --- /dev/null +++ b/tools/grit/grit/gather/chrome_html_unittest.py @@ -0,0 +1,523 @@ +#!/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''' + + +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': ''' + + + + + + + + + + ''', + + '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(''' + + + + + + + + + + ''')) + 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': ''' + + + + + + + ''', + + '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(''' + + + + + + + ''')) + 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 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 testFileResourcesNoFile(self): + '''Tests inlined image file resources without available high DPI assets.''' + + tmp_dir = util.TempDir({ + 'index.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(''' + + + + + + + + + + ''')) + 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': ''' + + + + + + + + + + ''', + + '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(''' + + + + + + + + + + ''')) + tmp_dir.CleanUp() + + def testRemoveUnsupportedScale(self): + '''Tests removing an unsupported scale factor from an explicit image-set.''' + + tmp_dir = util.TempDir({ + 'index.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(''' + + + + + + + + + + ''')) + tmp_dir.CleanUp() + + def testExpandVariablesInFilename(self): + ''' + Tests variable substitution in filenames while flattening images + with multiple scale factors. + ''' + + tmp_dir = util.TempDir({ + 'index.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(''' + + + + + + + + + + ''')) + 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 100755 index 0000000..864c9bd --- /dev/null +++ b/tools/grit/grit/gather/chrome_scaled_image.py @@ -0,0 +1,157 @@ +#!/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. + +'''Gatherer for . +''' + +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 = '\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 = '\x89PNG\r\n\x1a\n' + +'''Mandatory first chunk in order for the png to be valid.''' +_FIRST_CHUNK = 'IHDR' + +'''Special chunks to move immediately after the IHDR chunk. (so that the PNG +remains valid.) +''' +_SPECIAL_CHUNKS = frozenset('csCl npTc'.split()) + +'''Any ancillary chunk not in this list is deleted from the PNG.''' +_ANCILLARY_CHUNKS_TO_LEAVE = frozenset( + 'bKGD cHRM gAMA iCCP pHYs sBIT sRGB tRNS'.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 < '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 ''.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 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') + + # TODO(tdanderson): Search in descending order of all image scales + # instead of immediately falling back to 100. + # See crbug.com/503643. + 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(map(str, 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, *args): + 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 100755 index 0000000..4b0bbfc --- /dev/null +++ b/tools/grit/grit/gather/chrome_scaled_image_unittest.py @@ -0,0 +1,193 @@ +#!/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.''' + + +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 = ( + '\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52' + '\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53' + '\xde') +_PNG_FOOTER = ( + '\x00\x00\x00\x0c\x49\x44\x41\x54\x18\x57\x63\xf8\xff\xff\x3f\x00' + '\x05\xfe\x02\xfe\xa7\x35\x81\x84\x00\x00\x00\x00\x49\x45\x4e\x44' + '\xae\x42\x60\x82') + + +def _MakePNG(chunks): + pack_int32 = struct.Struct('>i').pack + chunks = [pack_int32(len(payload)) + type + payload + pack_int32(zlib.crc32(type + payload)) + for type, payload in chunks] + return _PNG_HEADER + ''.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.DataPack.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[m.group(1)] + for m in re.finditer(ur'(?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 '\n%s\n' % ( + _MakeFallbackAttr(fallback), '\n'.join(body)) + + +def _Structure(name, file, fallback=None): + return '' % ( + name, file, _MakeFallbackAttr(fallback)) + + +def _If(expr, *body): + return '\n%s\n' % (expr, '\n'.join(body)) + + +def _RunBuildTest(self, structures, inputs, expected_outputs, skip_rc=False, layout_fallback=''): + outputs = '\n'.join('' + % (context, ext, type, context, layout_fallback) + for ext, type in _OUTFILETYPES + for context in expected_outputs) + + infiles = { + 'in/in.grd': ''' + + + %s + + + %s + + + ''' % (outputs, structures), + } + for pngpath, pngdata in inputs.items(): + infiles['in/' + pngpath] = pngdata + class Options(object): + pass + with util.TempDir(infiles) 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([('AbCd', '')]) + t123a = _MakePNG([('EfGh', '')]) + d123b = _MakePNG([('IjKl', '')]) + _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([('AbCd', '')]), + 'tactile_100_percent/a.png': _MakePNG([('EfGh', '')]), + }, + {'tactile_123_percent': 'should fail before using this'}) + + def testLowresFallback(self): + png = _MakePNG([('Abcd', '')]) + png_with_csCl = _MakePNG([('csCl', ''),('Abcd', '')]) + 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([('AbCd', '')]) + t123a = _MakePNG([('EfGh', '')]) + d123b = _MakePNG([('IjKl', '')]) + _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"') diff --git a/tools/grit/grit/gather/igoogle_strings.py b/tools/grit/grit/gather/igoogle_strings.py new file mode 100755 index 0000000..79ed839 --- /dev/null +++ b/tools/grit/grit/gather/igoogle_strings.py @@ -0,0 +1,123 @@ +#!/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 ALL_ALL.xml format used by Igoogle plug-ins in Google Desktop.''' + +import StringIO +import re +import xml.sax +import xml.sax.handler +import xml.sax.saxutils + +from grit.gather import regexp +from grit import util +from grit import tclib + +# Placeholders can be defined in strings.xml files by putting the name of the +# placeholder between [![ and ]!] e.g. Hello [![USER]!] how are you +PLACEHOLDER_RE = re.compile('(\[!\[|\]!\])') + + +class IgoogleStringsContentHandler(xml.sax.handler.ContentHandler): + '''A very dumb parser for splitting the strings.xml file into translateable + and nontranslateable chunks.''' + + def __init__(self, parent): + self.curr_elem = '' + self.curr_text = '' + self.parent = parent + self.resource_name = '' + self.meaning = '' + self.translateable = True + + def startElement(self, name, attrs): + if (name != 'messagebundle'): + self.curr_elem = name + + attr_names = attrs.getQNames() + if 'name' in attr_names: + self.resource_name = attrs.getValueByQName('name') + + att_text = [] + for attr_name in attr_names: + att_text.append(' ') + att_text.append(attr_name) + att_text.append('=') + att_text.append( + xml.sax.saxutils.quoteattr(attrs.getValueByQName(attr_name))) + + self.parent._AddNontranslateableChunk("<%s%s>" % + (name, ''.join(att_text))) + + def characters(self, content): + if self.curr_elem != '': + self.curr_text += content + + def endElement(self, name): + if name != 'messagebundle': + self.parent.AddMessage(self.curr_text, self.resource_name, + self.meaning, self.translateable) + self.parent._AddNontranslateableChunk("\n" % name) + self.curr_elem = '' + self.curr_text = '' + self.resource_name = '' + self.meaning = '' + self.translateable = True + + def ignorableWhitespace(self, whitespace): + pass + + +class IgoogleStrings(regexp.RegexpGatherer): + '''Supports the ALL_ALL.xml format used by Igoogle gadgets.''' + + def AddMessage(self, msgtext, description, meaning, translateable): + if msgtext == '': + return + + msg = tclib.Message(description=description, meaning=meaning) + + unescaped_text = self.UnEscape(msgtext) + parts = PLACEHOLDER_RE.split(unescaped_text) + in_placeholder = False + for part in parts: + if part == '': + continue + elif part == '[![': + in_placeholder = True + elif part == ']!]': + in_placeholder = False + else: + if in_placeholder: + msg.AppendPlaceholder(tclib.Placeholder(part, '[![%s]!]' % part, + '(placeholder)')) + else: + msg.AppendText(part) + + self.skeleton_.append( + self.uberclique.MakeClique(msg, translateable=translateable)) + + # if statement needed because this is supposed to be idempotent (so never + # set back to false) + if translateable: + self.translatable_chunk_ = True + + # 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 SAX parser. + def Parse(self): + if self.have_parsed_: + return + self.have_parsed_ = True + + self.text_ = self._LoadInputFile().strip() + self._AddNontranslateableChunk(u'\n') + stream = StringIO.StringIO(self.text_) + handler = IgoogleStringsContentHandler(self) + xml.sax.parse(stream, handler) + self._AddNontranslateableChunk(u'\n') + + def Escape(self, text): + return util.EncodeCdata(text) diff --git a/tools/grit/grit/gather/igoogle_strings_unittest.py b/tools/grit/grit/gather/igoogle_strings_unittest.py new file mode 100755 index 0000000..3a4488c --- /dev/null +++ b/tools/grit/grit/gather/igoogle_strings_unittest.py @@ -0,0 +1,29 @@ +#!/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.igoogle_strings''' + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import unittest +import StringIO + +from grit.gather import igoogle_strings + +class IgoogleStringsUnittest(unittest.TestCase): + def testParsing(self): + original = '''Hello World''' + gatherer = igoogle_strings.IgoogleStrings(StringIO.StringIO(original)) + gatherer.Parse() + print len(gatherer.GetCliques()) + print gatherer.GetCliques()[0].GetMessage().GetRealContent() + self.failUnless(len(gatherer.GetCliques()) == 1) + self.failUnless(gatherer.Translate('en').replace('\n', '') == original) + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/gather/interface.py b/tools/grit/grit/gather/interface.py new file mode 100755 index 0000000..c277d37 --- /dev/null +++ b/tools/grit/grit/gather/interface.py @@ -0,0 +1,171 @@ +#!/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. + +'''Interface for all gatherers. +''' + + +import os.path +import types + +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 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, types.StringTypes): + 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 100755 index 0000000..6370b10 --- /dev/null +++ b/tools/grit/grit/gather/json_loader.py @@ -0,0 +1,26 @@ +#!/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. + + +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): + '''Returns the parsed JSON data.''' + return self._data diff --git a/tools/grit/grit/gather/muppet_strings.py b/tools/grit/grit/gather/muppet_strings.py new file mode 100755 index 0000000..ab2f08a --- /dev/null +++ b/tools/grit/grit/gather/muppet_strings.py @@ -0,0 +1,133 @@ +#!/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 "strings.xml" format used by Muppet plug-ins in Google Desktop.''' + +import StringIO +import xml.sax +import xml.sax.handler +import xml.sax.saxutils + +from grit import lazy_re +from grit import tclib +from grit import util +from grit.gather import regexp + + +# Placeholders can be defined in strings.xml files by putting the name of the +# placeholder between [![ and ]!] e.g. Hello [![USER]!] how are you +PLACEHOLDER_RE = lazy_re.compile('(\[!\[|\]!\])') + + +class MuppetStringsContentHandler(xml.sax.handler.ContentHandler): + '''A very dumb parser for splitting the strings.xml file into translateable + and nontranslateable chunks.''' + + def __init__(self, parent): + self.curr_elem = '' + self.curr_text = '' + self.parent = parent + self.description = '' + self.meaning = '' + self.translateable = True + + def startElement(self, name, attrs): + if (name != 'strings'): + self.curr_elem = name + + attr_names = attrs.getQNames() + if 'desc' in attr_names: + self.description = attrs.getValueByQName('desc') + if 'meaning' in attr_names: + self.meaning = attrs.getValueByQName('meaning') + if 'translateable' in attr_names: + value = attrs.getValueByQName('translateable') + if value.lower() not in ['true', 'yes']: + self.translateable = False + + att_text = [] + for attr_name in attr_names: + att_text.append(' ') + att_text.append(attr_name) + att_text.append('=') + att_text.append( + xml.sax.saxutils.quoteattr(attrs.getValueByQName(attr_name))) + + self.parent._AddNontranslateableChunk("<%s%s>" % + (name, ''.join(att_text))) + + def characters(self, content): + if self.curr_elem != '': + self.curr_text += content + + def endElement(self, name): + if name != 'strings': + self.parent.AddMessage(self.curr_text, self.description, + self.meaning, self.translateable) + self.parent._AddNontranslateableChunk("\n" % name) + self.curr_elem = '' + self.curr_text = '' + self.description = '' + self.meaning = '' + self.translateable = True + + def ignorableWhitespace(self, whitespace): + pass + +class MuppetStrings(regexp.RegexpGatherer): + '''Supports the strings.xml format used by Muppet gadgets.''' + + def AddMessage(self, msgtext, description, meaning, translateable): + if msgtext == '': + return + + msg = tclib.Message(description=description, meaning=meaning) + + unescaped_text = self.UnEscape(msgtext) + parts = PLACEHOLDER_RE.split(unescaped_text) + in_placeholder = False + for part in parts: + if part == '': + continue + elif part == '[![': + in_placeholder = True + elif part == ']!]': + in_placeholder = False + else: + if in_placeholder: + msg.AppendPlaceholder(tclib.Placeholder(part, '[![%s]!]' % part, + '(placeholder)')) + else: + msg.AppendText(part) + + self.skeleton_.append( + self.uberclique.MakeClique(msg, translateable=translateable)) + + # if statement needed because this is supposed to be idempotent (so never + # set back to false) + if translateable: + self.translatable_chunk_ = True + + # 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 SAX parser. + def Parse(self): + if self.have_parsed_: + return + self.have_parsed_ = True + + text = self._LoadInputFile().encode(self.encoding) + if util.IsExtraVerbose(): + print text + self.text_ = text.strip() + + self._AddNontranslateableChunk(u'\n') + stream = StringIO.StringIO(self.text_) + handler = MuppetStringsContentHandler(self) + xml.sax.parse(stream, handler) + self._AddNontranslateableChunk(u'\n') + + def Escape(self, text): + return util.EncodeCdata(text) diff --git a/tools/grit/grit/gather/muppet_strings_unittest.py b/tools/grit/grit/gather/muppet_strings_unittest.py new file mode 100755 index 0000000..adf66b65 --- /dev/null +++ b/tools/grit/grit/gather/muppet_strings_unittest.py @@ -0,0 +1,67 @@ +#!/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.muppet_strings''' + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import unittest +import StringIO + +from grit.gather import muppet_strings + +class MuppetStringsUnittest(unittest.TestCase): + def testParsing(self): + original = '''hello!YEEEESSS!!!''' + gatherer = muppet_strings.MuppetStrings(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(len(gatherer.GetCliques()) == 2) + self.failUnless(gatherer.Translate('en').replace('\n', '') == original) + + def testEscapingAndLinebreaks(self): + original = ('''\ + +Hello +there +how +are +you? 4 < 6 +''') + gatherer = muppet_strings.MuppetStrings(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(gatherer.GetCliques()[0].translateable) + self.failUnless(len(gatherer.GetCliques()) == 2) + self.failUnless(gatherer.GetCliques()[0].GetMessage().GetRealContent() == + 'Hello\nthere\nhow\nare\nyou?') + self.failUnless(gatherer.GetCliques()[0].GetMessage().GetDescription() == 'Howdie') + self.failUnless(gatherer.GetCliques()[1].GetMessage().GetRealContent() == + '4 < 6') + self.failUnless(gatherer.GetCliques()[1].GetMessage().GetMeaning() == 'bingo') + + def testPlaceholders(self): + original = "Hello [![USER]!] how are you? [![HOUR]!]:[![MINUTE]!]" + gatherer = muppet_strings.MuppetStrings(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(gatherer.GetCliques()[0].translateable) + msg = gatherer.GetCliques()[0].GetMessage() + self.failUnless(len(msg.GetPlaceholders()) == 3) + ph = msg.GetPlaceholders()[0] + self.failUnless(ph.GetOriginal() == '[![USER]!]') + self.failUnless(ph.GetPresentation() == 'USER') + + def testTranslateable(self): + original = "Yo yo hi there" + gatherer = muppet_strings.MuppetStrings(StringIO.StringIO(original)) + gatherer.Parse() + msg = gatherer.GetCliques()[0].GetMessage() + self.failUnless(msg.GetRealContent() == "Yo yo hi there") + self.failUnless(not gatherer.GetCliques()[0].translateable) + +if __name__ == '__main__': + unittest.main() + diff --git a/tools/grit/grit/gather/policy_json.py b/tools/grit/grit/gather/policy_json.py new file mode 100755 index 0000000..0dcd831 --- /dev/null +++ b/tools/grit/grit/gather/policy_json.py @@ -0,0 +1,251 @@ +#!/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 "policy_templates.json" format used by the policy template +generator as a source for generating ADM,ADMX,etc files.''' + +import types +import sys + +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 and labels 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 _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: + Placeholder textExample value + 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 + msg.AppendPlaceholder(tclib.Placeholder( + placeholder.attributes['name'].value, + ''.join(text).strip(), + ''.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 = '' + string + '' + try: + node = minidom.parseString(xml).childNodes[0] + except ExpatError: + reason = '''Input isn't valid XML (has < & > been escaped?): ''' + string + raise 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 nodes. + ''' + att_text = [] + if node.attributes: + items = node.attributes.items() + items.sort() + for key, value in 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("" % 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', + } + if item_type == 'policy': + return '%s of the policy named %s' % (key_map[key], item['name']) + elif item_type == 'enum_item': + return ('%s of the option named %s in policy %s' % + (key_map[key], item['name'], parent_item['name'])) + else: + raise Exception('Unexpected type %s' % item_type) + + 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': " % key) + if key in ('desc', 'caption', 'label'): + self._AddNontranslateableChunk("'''") + self._ParseMessage( + item[key], + self._GetDescription(item, item_type, parent_item, key)) + self._AddNontranslateableChunk("''',\n") + else: + str_val = item[key] + if type(str_val) == types.StringType: + str_val = "'%s'" % self.Escape(str_val) + else: + str_val = str(str_val) + self._AddNontranslateableChunk(str_val + ',\n') + + 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 item1 in items: + self._AddIndentedNontranslateableChunk(depth, "{\n") + for key in item1.keys(): + if key == 'items': + self._AddIndentedNontranslateableChunk(depth + 1, "'items': [\n") + self._AddItems(item1['items'], 'enum_item', item1, depth + 2) + self._AddIndentedNontranslateableChunk(depth + 1, "],\n") + elif key == 'policies': + self._AddIndentedNontranslateableChunk(depth + 1, "'policies': [\n") + self._AddItems(item1['policies'], 'policy', item1, depth + 2) + self._AddIndentedNontranslateableChunk(depth + 1, "],\n") + else: + self._AddPolicyKey(item1, item_type, parent_item, key, depth + 1) + self._AddIndentedNontranslateableChunk(depth, "},\n") + + def _AddMessages(self): + '''Processed and adds the 'messages' section to the output.''' + self._AddNontranslateableChunk(" 'messages': {\n") + for name, message in self.data['messages'].iteritems(): + self._AddNontranslateableChunk(" '%s': {\n" % name) + self._AddNontranslateableChunk(" 'text': '''") + self._ParseMessage(message['text'], message['desc']) + self._AddNontranslateableChunk("'''\n") + self._AddNontranslateableChunk(" },\n") + 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._AddMessages() + self._AddNontranslateableChunk('\n}') + + def Escape(self, text): + # \ -> \\ + # ' -> \' + # " -> \" + return text.replace('\\', '\\\\').replace('"', '\\"').replace("'", "\\'") diff --git a/tools/grit/grit/gather/policy_json_unittest.py b/tools/grit/grit/gather/policy_json_unittest.py new file mode 100755 index 0000000..f536f5d --- /dev/null +++ b/tools/grit/grit/gather/policy_json_unittest.py @@ -0,0 +1,190 @@ +#!/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''' + +import os +import re +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import unittest +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'].iteritems(): + del message['desc'] + return expected + + def testEmpty(self): + original = "{'policy_definitions': [], 'messages': {}}" + gatherer = policy_json.PolicyJson(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(len(gatherer.GetCliques()) == 0) + self.failUnless(eval(original) == eval(gatherer.Translate('en'))) + + def testGeneralPolicy(self): + original = ( + "{" + " 'policy_definitions': [" + " {" + " 'name': 'HomepageLocation'," + " 'type': 'string'," + " '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'," + " }," + " ]," + " 'messages': {" + " 'msg_identifier': {" + " 'text': 'nothing special 3'," + " 'desc': 'nothing special descr 3'," + " }" + " }" + "}") + gatherer = policy_json.PolicyJson(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(len(gatherer.GetCliques()) == 4) + expected = self.GetExpectedOutput(original) + self.failUnless(expected == eval(gatherer.Translate('en'))) + + def testEnum(self): + original = ( + "{" + " 'policy_definitions': [" + " {" + " 'name': 'Policy1'," + " 'items': [" + " {" + " 'name': 'Item1'," + " 'caption': 'nothing special'," + " }" + " ]" + " }," + " ]," + " 'messages': {}" + "}") + gatherer = policy_json.PolicyJson(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(len(gatherer.GetCliques()) == 1) + expected = self.GetExpectedOutput(original) + self.failUnless(expected == eval(gatherer.Translate('en'))) + + def testSubPolicy(self): + original = ( + "{" + " 'policy_definitions': [" + " {" + " 'policies': [" + " {" + " 'name': 'Policy1'," + " 'caption': 'nothing special'," + " }" + " ]" + " }," + " ]," + " 'messages': {}" + "}") + gatherer = policy_json.PolicyJson(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(len(gatherer.GetCliques()) == 1) + expected = self.GetExpectedOutput(original) + self.failUnless(expected == eval(gatherer.Translate('en'))) + + def testEscapingAndLineBreaks(self): + original = """{ + 'policy_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.StringIO(original)) + gatherer.Parse() + self.failUnless(len(gatherer.GetCliques()) == 6) + expected = self.GetExpectedOutput(original) + self.failUnless(expected == eval(gatherer.Translate('en'))) + + def testPlaceholders(self): + original = """{ + 'policy_definitions': [ + { + 'name': 'Policy1', + 'caption': '''Please install + $1Google Chrome.''', + }, + ], + 'messages': {} +}""" + gatherer = policy_json.PolicyJson(StringIO.StringIO(original)) + gatherer.Parse() + self.failUnless(len(gatherer.GetCliques()) == 1) + expected = eval(re.sub('', '$1', original)) + self.failUnless(expected == eval(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() == '$1') + self.failUnless(ph.GetPresentation() == 'PRODUCT_NAME') + self.failUnless(ph.GetExample() == 'Google Chrome') + + def testGetDescription(self): + gatherer = policy_json.PolicyJson({}) + self.assertEquals( + gatherer._GetDescription({'name': 'Policy1'}, 'policy', None, 'desc'), + 'Description of the policy named Policy1') + self.assertEquals( + gatherer._GetDescription({'name': 'Plcy2'}, 'policy', None, 'caption'), + 'Caption of the policy named Plcy2') + self.assertEquals( + gatherer._GetDescription({'name': 'Plcy3'}, 'policy', None, 'label'), + 'Label of the policy named Plcy3') + self.assertEquals( + gatherer._GetDescription({'name': 'Item'}, 'enum_item', + {'name': 'Policy'}, 'caption'), + 'Caption of the option named Item in policy Policy') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/grit/grit/gather/rc.py b/tools/grit/grit/gather/rc.py new file mode 100755 index 0000000..f1e8982 --- /dev/null +++ b/tools/grit/grit/gather/rc.py @@ -0,0 +1,343 @@ +#!/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 gathering resources from RC files. +''' + + +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 +# 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(''' + # The dialog's ID in the first line + (?P[A-Z0-9_]+)\s+DIALOG(EX)? + | + # The caption of the dialog + (?PCAPTION)\s+"(?P.*?([^"]|""))"\s + | + # Lines for controls that have text and an ID + \s+(?P[A-Z]+)\s+"(?P.*?([^"]|"")?)"\s*,\s*(?P[A-Z0-9_]+)\s*, + | + # Lines for controls that have text only + \s+(?P[A-Z]+)\s+"(?P.*?([^"]|"")?)"\s*, + | + # Lines for controls that reference other resources + \s+[A-Z]+\s+[A-Z0-9_]+\s*,\s*(?P[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[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(''' + # Match the MENU ID on the first line + ^(?P[A-Z0-9_]+)\s+MENU + | + # Match the translateable caption for a popup menu + POPUP\s+"(?P.*?([^"]|""))"\s + | + # Match the caption & ID of a MENUITEM + MENUITEM\s+"(?P.*?([^"]|""))"\s*,\s*(?P[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: " + # VALUE "FileDescription", "TODO: " + # VALUE "FileVersion", "1.0.0.1" + # VALUE "LegalCopyright", "TODO: (c) . All rights reserved." + # VALUE "InternalName", "res_format_test.dll" + # VALUE "OriginalFilename", "res_format_test.dll" + # VALUE "ProductName", "TODO: " + # 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(''' + # Match the ID on the first line + ^(?P[A-Z0-9_]+)\s+VERSIONINFO + | + # Match all potentially translateable VALUE sections + \s+VALUE\s+" + ( + CompanyName|FileDescription|LegalCopyright| + ProductName|Comments|LegalTrademarks + )",\s+"(?P.*?([^"]|""))"\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(''' + ^(?P[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(''' + # Match the ID on the first line + ^(?P[A-Z0-9_]+)\s+ACCELERATORS\s+ + | + # Match accelerators specified as VK_XXX + \s+VK_[A-Z0-9_]+,\s*(?P[A-Z0-9_]+)\s*, + | + # Match accelerators specified as e.g. "^C" + \s+"[^"]*",\s+(?P[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 100755 index 0000000..c4be35e --- /dev/null +++ b/tools/grit/grit/gather/rc_unittest.py @@ -0,0 +1,370 @@ +#!/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''' + + +import os +import sys +if __name__ == '__main__': + sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +import unittest +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.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.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.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.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.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.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: " + VALUE "FileDescription", "TODO: " + VALUE "FileVersion", "1.0.0.1" + VALUE "LegalCopyright", "TODO: (c) . All rights reserved." + VALUE "InternalName", "res_format_test.dll" + VALUE "OriginalFilename", "res_format_test.dll" + VALUE "ProductName", "TODO: " + 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.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.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.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.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.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.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.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.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 100755 index 0000000..30488a6 --- /dev/null +++ b/tools/grit/grit/gather/regexp.py @@ -0,0 +1,85 @@ +#!/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. + +'''A baseclass for simple gatherers based on regular expressions. +''' + +import re + +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 self.DescriptionMapping_.has_key(string): + 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 = groups.keys() + keys.sort() + 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 100755 index 0000000..38b504c --- /dev/null +++ b/tools/grit/grit/gather/skeleton_gatherer.py @@ -0,0 +1,147 @@ +#!/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. + +'''A baseclass for simple gatherers that store their gathered resource in a +list. +''' + +import types + +from grit.gather import interface +from grit import clique +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], types.StringTypes): + if skeleton_gatherer: + # Make sure the skeleton is like the original + assert(isinstance(skeleton_gatherer.skeleton_[ix], types.StringTypes)) + 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], + types.StringTypes)) + 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 100755 index 0000000..3487251 --- /dev/null +++ b/tools/grit/grit/gather/tr_html.py @@ -0,0 +1,745 @@ +#!/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. + +'''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: + + blabla (not parseable because attributes are invalid) + +
[LOTSOFSTUFF]
(not parseable because closing + 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 element, or + the 'value' attribute of a ,