Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


SSL 0091.patch   Interaktion und
Portierbarkeitunbekannt

 
Spracherkennung für: .patch vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

From: Nico Grunbaum <na-g@nostrum.com>
Date: Fri, 30 Apr 2021 21:51:00 +0000
Subject: Bug 1654112 - Add grit dep for building webrtc on android; r=mjf

Differential Revision: https://phabricator.services.mozilla.com/D114027
Mercurial Revision: https://hg.mozilla.org/mozilla-central/rev/3cce5e6938f0df87bd9ab12a5f556aceb93dfa1d
---
 tools/grit/.gitignore                         |    1 +
 tools/grit/BUILD.gn                           |   48 +
 tools/grit/MANIFEST.in                        |    3 +
 tools/grit/OWNERS                             |    8 +
 tools/grit/PRESUBMIT.py                       |   22 +
 tools/grit/README.md                          |   19 +
 tools/grit/grit.py                            |   31 +
 tools/grit/grit/__init__.py                   |   19 +
 tools/grit/grit/clique.py                     |  491 +++
 tools/grit/grit/clique_unittest.py            |  265 ++
 tools/grit/grit/constants.py                  |   23 +
 tools/grit/grit/exception.py                  |  139 +
 tools/grit/grit/extern/BogoFP.py              |   22 +
 tools/grit/grit/extern/FP.py                  |   72 +
 tools/grit/grit/extern/__init__.py            |    0
 tools/grit/grit/extern/tclib.py               |  503 +++
 tools/grit/grit/format/__init__.py            |    8 +
 tools/grit/grit/format/android_xml.py         |  212 ++
 .../grit/grit/format/android_xml_unittest.py  |  149 +
 tools/grit/grit/format/c_format.py            |   95 +
 tools/grit/grit/format/c_format_unittest.py   |   81 +
 .../grit/grit/format/chrome_messages_json.py  |   59 +
 .../format/chrome_messages_json_unittest.py   |  190 +
 tools/grit/grit/format/data_pack.py           |  321 ++
 tools/grit/grit/format/data_pack_unittest.py  |  102 +
 .../grit/grit/format/gen_predetermined_ids.py |  144 +
 .../format/gen_predetermined_ids_unittest.py  |   46 +
 tools/grit/grit/format/gzip_string.py         |   46 +
 .../grit/grit/format/gzip_string_unittest.py  |   65 +
 tools/grit/grit/format/html_inline.py         |  602 ++++
 .../grit/grit/format/html_inline_unittest.py  |  927 +++++
 tools/grit/grit/format/minifier.py            |   45 +
 .../grit/grit/format/policy_templates_json.py |   26 +
 .../format/policy_templates_json_unittest.py  |  207 ++
 tools/grit/grit/format/rc.py                  |  474 +++
 tools/grit/grit/format/rc_header.py           |   48 +
 tools/grit/grit/format/rc_header_unittest.py  |  138 +
 tools/grit/grit/format/rc_unittest.py         |  415 +++
 tools/grit/grit/format/resource_map.py        |  159 +
 .../grit/grit/format/resource_map_unittest.py |  345 ++
 tools/grit/grit/gather/__init__.py            |    8 +
 tools/grit/grit/gather/admin_template.py      |   62 +
 .../grit/gather/admin_template_unittest.py    |  115 +
 tools/grit/grit/gather/chrome_html.py         |  377 ++
 .../grit/grit/gather/chrome_html_unittest.py  |  610 ++++
 tools/grit/grit/gather/chrome_scaled_image.py |  157 +
 .../gather/chrome_scaled_image_unittest.py    |  209 ++
 tools/grit/grit/gather/interface.py           |  172 +
 tools/grit/grit/gather/json_loader.py         |   27 +
 tools/grit/grit/gather/policy_json.py         |  325 ++
 .../grit/grit/gather/policy_json_unittest.py  |  347 ++
 tools/grit/grit/gather/rc.py                  |  343 ++
 tools/grit/grit/gather/rc_unittest.py         |  372 ++
 tools/grit/grit/gather/regexp.py              |   82 +
 tools/grit/grit/gather/skeleton_gatherer.py   |  149 +
 tools/grit/grit/gather/tr_html.py             |  743 ++++
 tools/grit/grit/gather/tr_html_unittest.py    |  524 +++
 tools/grit/grit/gather/txt.py                 |   38 +
 tools/grit/grit/gather/txt_unittest.py        |   35 +
 tools/grit/grit/grd_reader.py                 |  238 ++
 tools/grit/grit/grd_reader_unittest.py        |  346 ++
 tools/grit/grit/grit-todo.xml                 |   62 +
 tools/grit/grit/grit_runner.py                |  334 ++
 tools/grit/grit/grit_runner_unittest.py       |   42 +
 tools/grit/grit/lazy_re.py                    |   46 +
 tools/grit/grit/lazy_re_unittest.py           |   40 +
 tools/grit/grit/node/__init__.py              |    8 +
 tools/grit/grit/node/base.py                  |  670 ++++
 tools/grit/grit/node/base_unittest.py         |  259 ++
 tools/grit/grit/node/brotli_util.py           |   29 +
 tools/grit/grit/node/custom/__init__.py       |    8 +
 tools/grit/grit/node/custom/filename.py       |   29 +
 .../grit/node/custom/filename_unittest.py     |   34 +
 tools/grit/grit/node/empty.py                 |   64 +
 tools/grit/grit/node/include.py               |  170 +
 tools/grit/grit/node/include_unittest.py      |  134 +
 tools/grit/grit/node/mapping.py               |   60 +
 tools/grit/grit/node/message.py               |  362 ++
 tools/grit/grit/node/message_unittest.py      |  380 ++
 tools/grit/grit/node/misc.py                  |  707 ++++
 tools/grit/grit/node/misc_unittest.py         |  590 ++++
 tools/grit/grit/node/mock_brotli.py           |   10 +
 tools/grit/grit/node/node_io.py               |  117 +
 tools/grit/grit/node/node_io_unittest.py      |  182 +
 tools/grit/grit/node/structure.py             |  375 ++
 tools/grit/grit/node/structure_unittest.py    |  178 +
 tools/grit/grit/node/variant.py               |   41 +
 tools/grit/grit/pseudo.py                     |  129 +
 tools/grit/grit/pseudo_rtl.py                 |  104 +
 tools/grit/grit/pseudo_unittest.py            |   55 +
 tools/grit/grit/shortcuts.py                  |   93 +
 tools/grit/grit/shortcuts_unittest.py         |   79 +
 tools/grit/grit/tclib.py                      |  246 ++
 tools/grit/grit/tclib_unittest.py             |  180 +
 tools/grit/grit/test_suite_all.py             |   34 +
 tools/grit/grit/testdata/GoogleDesktop.adm    |  945 +++++
 tools/grit/grit/testdata/README.txt           |   87 +
 tools/grit/grit/testdata/about.html           |   45 +
 tools/grit/grit/testdata/android.xml          |   24 +
 tools/grit/grit/testdata/bad_browser.html     |   16 +
 tools/grit/grit/testdata/browser.html         |   42 +
 tools/grit/grit/testdata/buildinfo.grd        |   46 +
 tools/grit/grit/testdata/cache_prefix.html    |   24 +
 .../grit/grit/testdata/cache_prefix_file.html |   25 +
 tools/grit/grit/testdata/chat_result.html     |   24 +
 .../chrome/app/generated_resources.grd        |  199 ++
 tools/grit/grit/testdata/chrome_html.html     |    6 +
 .../grit/testdata/default_100_percent/a.png   |  Bin 0 -> 159 bytes
 .../grit/testdata/default_100_percent/b.png   |    1 +
 tools/grit/grit/testdata/del_footer.html      |    8 +
 tools/grit/grit/testdata/del_header.html      |   60 +
 tools/grit/grit/testdata/deleted.html         |   21 +
 tools/grit/grit/testdata/depfile.grd          |   18 +
 tools/grit/grit/testdata/details.html         |   10 +
 .../grit/testdata/duplicate-name-input.xml    |   26 +
 tools/grit/grit/testdata/email_result.html    |   34 +
 tools/grit/grit/testdata/email_thread.html    |   10 +
 tools/grit/grit/testdata/error.html           |    8 +
 tools/grit/grit/testdata/explicit_web.html    |   11 +
 tools/grit/grit/testdata/footer.html          |   14 +
 .../grit/testdata/generated_resources_fr.xtb  | 3079 +++++++++++++++++
 .../grit/testdata/generated_resources_iw.xtb  |    4 +
 .../grit/testdata/generated_resources_no.xtb  |    4 +
 tools/grit/grit/testdata/grit_part.grdp       |    5 +
 tools/grit/grit/testdata/header.html          |   39 +
 tools/grit/grit/testdata/homepage.html        |   37 +
 tools/grit/grit/testdata/hover.html           |  177 +
 tools/grit/grit/testdata/include_test.html    |   31 +
 tools/grit/grit/testdata/included_sample.html |    1 +
 tools/grit/grit/testdata/indexing_speed.html  |   58 +
 tools/grit/grit/testdata/install_prefs.html   |   92 +
 tools/grit/grit/testdata/install_prefs2.html  |   52 +
 .../grit/testdata/klonk-alternate-skeleton.rc |  Bin 0 -> 1088 bytes
 tools/grit/grit/testdata/klonk.ico            |  Bin 0 -> 766 bytes
 tools/grit/grit/testdata/klonk.rc             |  Bin 0 -> 9824 bytes
 .../grit/grit/testdata/ko_oem_enable_bug.html |    1 +
 .../grit/testdata/ko_oem_non_admin_bug.html   |    1 +
 tools/grit/grit/testdata/mini.html            |   36 +
 tools/grit/grit/testdata/oem_enable.html      |  106 +
 tools/grit/grit/testdata/oem_non_admin.html   |   39 +
 tools/grit/grit/testdata/onebox.html          |   21 +
 tools/grit/grit/testdata/oneclick.html        |   34 +
 tools/grit/grit/testdata/password.html        |   37 +
 tools/grit/grit/testdata/preferences.html     |  234 ++
 tools/grit/grit/testdata/preprocess_test.html |    7 +
 tools/grit/grit/testdata/privacy.html         |   35 +
 tools/grit/grit/testdata/quit_apps.html       |   49 +
 tools/grit/grit/testdata/recrawl.html         |   30 +
 tools/grit/grit/testdata/resource_ids         |   13 +
 tools/grit/grit/testdata/script.html          |   38 +
 tools/grit/grit/testdata/searchbox.html       |   22 +
 tools/grit/grit/testdata/sidebar_h.html       |   82 +
 tools/grit/grit/testdata/sidebar_v.html       |  267 ++
 tools/grit/grit/testdata/simple-input.xml     |   52 +
 tools/grit/grit/testdata/simple.html          |    3 +
 tools/grit/grit/testdata/source.rc            |   57 +
 .../grit/testdata/special_100_percent/a.png   |  Bin 0 -> 159 bytes
 tools/grit/grit/testdata/status.html          |   44 +
 .../grit/testdata/structure_variables.html    |    4 +
 tools/grit/grit/testdata/substitute.grd       |   31 +
 tools/grit/grit/testdata/substitute.xmb       |   10 +
 .../grit/grit/testdata/substitute_no_ids.grd  |   31 +
 tools/grit/grit/testdata/substitute_tmpl.grd  |   31 +
 tools/grit/grit/testdata/test_css.css         |    1 +
 tools/grit/grit/testdata/test_html.html       |    1 +
 tools/grit/grit/testdata/test_js.js           |    1 +
 tools/grit/grit/testdata/test_svg.svg         |    1 +
 tools/grit/grit/testdata/test_text.txt        |    1 +
 tools/grit/grit/testdata/time_related.html    |   11 +
 tools/grit/grit/testdata/toolbar_about.html   |  138 +
 .../grit/testdata/tools/grit/resource_ids     |  176 +
 tools/grit/grit/testdata/transl.rc            |   56 +
 tools/grit/grit/testdata/versions.html        |    7 +
 tools/grit/grit/testdata/whitelist.txt        |    4 +
 .../grit/testdata/whitelist_resources.grd     |   54 +
 .../grit/grit/testdata/whitelist_strings.grd  |   23 +
 tools/grit/grit/tool/__init__.py              |    8 +
 tools/grit/grit/tool/android2grd.py           |  484 +++
 tools/grit/grit/tool/android2grd_unittest.py  |  181 +
 tools/grit/grit/tool/build.py                 |  556 +++
 tools/grit/grit/tool/build_unittest.py        |  341 ++
 tools/grit/grit/tool/buildinfo.py             |   78 +
 tools/grit/grit/tool/buildinfo_unittest.py    |   90 +
 tools/grit/grit/tool/count.py                 |   52 +
 tools/grit/grit/tool/diff_structures.py       |  119 +
 .../grit/tool/diff_structures_unittest.py     |   46 +
 tools/grit/grit/tool/interface.py             |   62 +
 tools/grit/grit/tool/menu_from_parts.py       |   79 +
 tools/grit/grit/tool/newgrd.py                |   85 +
 tools/grit/grit/tool/newgrd_unittest.py       |   51 +
 tools/grit/grit/tool/postprocess_interface.py |   29 +
 tools/grit/grit/tool/postprocess_unittest.py  |   64 +
 tools/grit/grit/tool/preprocess_interface.py  |   25 +
 tools/grit/grit/tool/preprocess_unittest.py   |   50 +
 tools/grit/grit/tool/rc2grd.py                |  418 +++
 tools/grit/grit/tool/rc2grd_unittest.py       |  163 +
 tools/grit/grit/tool/resize.py                |  295 ++
 tools/grit/grit/tool/test.py                  |   24 +
 tools/grit/grit/tool/transl2tc.py             |  251 ++
 tools/grit/grit/tool/transl2tc_unittest.py    |  133 +
 tools/grit/grit/tool/unit.py                  |   43 +
 .../grit/tool/update_resource_ids/__init__.py |  305 ++
 .../grit/tool/update_resource_ids/assigner.py |  286 ++
 .../update_resource_ids/assigner_unittest.py  |  154 +
 .../grit/tool/update_resource_ids/common.py   |  101 +
 .../grit/tool/update_resource_ids/parser.py   |  231 ++
 .../grit/tool/update_resource_ids/reader.py   |   83 +
 tools/grit/grit/tool/xmb.py                   |  295 ++
 tools/grit/grit/tool/xmb_unittest.py          |  132 +
 tools/grit/grit/util.py                       |  691 ++++
 tools/grit/grit/util_unittest.py              |  118 +
 tools/grit/grit/xtb_reader.py                 |  140 +
 tools/grit/grit/xtb_reader_unittest.py        |  110 +
 tools/grit/grit_info.py                       |  173 +
 tools/grit/grit_rule.gni                      |  485 +++
 tools/grit/minify_with_uglify.py              |   44 +
 tools/grit/minimize_css.py                    |  105 +
 tools/grit/minimize_css_unittest.py           |   58 +
 tools/grit/pak_util.py                        |  223 ++
 tools/grit/repack.gni                         |  189 +
 tools/grit/setup.py                           |   46 +
 tools/grit/stamp_grit_sources.py              |   57 +
 tools/grit/third_party/six/LICENSE            |   18 +
 tools/grit/third_party/six/README             |   16 +
 tools/grit/third_party/six/README.chromium    |   13 +
 tools/grit/third_party/six/__init__.py        |  868 +++++
 226 files changed, 33440 insertions(+)
 create mode 100644 tools/grit/.gitignore
 create mode 100644 tools/grit/BUILD.gn
 create mode 100644 tools/grit/MANIFEST.in
 create mode 100644 tools/grit/OWNERS
 create mode 100644 tools/grit/PRESUBMIT.py
 create mode 100644 tools/grit/README.md
 create mode 100644 tools/grit/grit.py
 create mode 100644 tools/grit/grit/__init__.py
 create mode 100644 tools/grit/grit/clique.py
 create mode 100644 tools/grit/grit/clique_unittest.py
 create mode 100644 tools/grit/grit/constants.py
 create mode 100644 tools/grit/grit/exception.py
 create mode 100644 tools/grit/grit/extern/BogoFP.py
 create mode 100644 tools/grit/grit/extern/FP.py
 create mode 100644 tools/grit/grit/extern/__init__.py
 create mode 100644 tools/grit/grit/extern/tclib.py
 create mode 100644 tools/grit/grit/format/__init__.py
 create mode 100644 tools/grit/grit/format/android_xml.py
 create mode 100644 tools/grit/grit/format/android_xml_unittest.py
 create mode 100644 tools/grit/grit/format/c_format.py
 create mode 100644 tools/grit/grit/format/c_format_unittest.py
 create mode 100644 tools/grit/grit/format/chrome_messages_json.py
 create mode 100644 tools/grit/grit/format/chrome_messages_json_unittest.py
 create mode 100644 tools/grit/grit/format/data_pack.py
 create mode 100644 tools/grit/grit/format/data_pack_unittest.py
 create mode 100644 tools/grit/grit/format/gen_predetermined_ids.py
 create mode 100644 tools/grit/grit/format/gen_predetermined_ids_unittest.py
 create mode 100644 tools/grit/grit/format/gzip_string.py
 create mode 100644 tools/grit/grit/format/gzip_string_unittest.py
 create mode 100644 tools/grit/grit/format/html_inline.py
 create mode 100644 tools/grit/grit/format/html_inline_unittest.py
 create mode 100644 tools/grit/grit/format/minifier.py
 create mode 100644 tools/grit/grit/format/policy_templates_json.py
 create mode 100644 tools/grit/grit/format/policy_templates_json_unittest.py
 create mode 100644 tools/grit/grit/format/rc.py
 create mode 100644 tools/grit/grit/format/rc_header.py
 create mode 100644 tools/grit/grit/format/rc_header_unittest.py
 create mode 100644 tools/grit/grit/format/rc_unittest.py
 create mode 100644 tools/grit/grit/format/resource_map.py
 create mode 100644 tools/grit/grit/format/resource_map_unittest.py
 create mode 100644 tools/grit/grit/gather/__init__.py
 create mode 100644 tools/grit/grit/gather/admin_template.py
 create mode 100644 tools/grit/grit/gather/admin_template_unittest.py
 create mode 100644 tools/grit/grit/gather/chrome_html.py
 create mode 100644 tools/grit/grit/gather/chrome_html_unittest.py
 create mode 100644 tools/grit/grit/gather/chrome_scaled_image.py
 create mode 100644 tools/grit/grit/gather/chrome_scaled_image_unittest.py
 create mode 100644 tools/grit/grit/gather/interface.py
 create mode 100644 tools/grit/grit/gather/json_loader.py
 create mode 100644 tools/grit/grit/gather/policy_json.py
 create mode 100644 tools/grit/grit/gather/policy_json_unittest.py
 create mode 100644 tools/grit/grit/gather/rc.py
 create mode 100644 tools/grit/grit/gather/rc_unittest.py
 create mode 100644 tools/grit/grit/gather/regexp.py
 create mode 100644 tools/grit/grit/gather/skeleton_gatherer.py
 create mode 100644 tools/grit/grit/gather/tr_html.py
 create mode 100644 tools/grit/grit/gather/tr_html_unittest.py
 create mode 100644 tools/grit/grit/gather/txt.py
 create mode 100644 tools/grit/grit/gather/txt_unittest.py
 create mode 100644 tools/grit/grit/grd_reader.py
 create mode 100644 tools/grit/grit/grd_reader_unittest.py
 create mode 100644 tools/grit/grit/grit-todo.xml
 create mode 100644 tools/grit/grit/grit_runner.py
 create mode 100644 tools/grit/grit/grit_runner_unittest.py
 create mode 100644 tools/grit/grit/lazy_re.py
 create mode 100644 tools/grit/grit/lazy_re_unittest.py
 create mode 100644 tools/grit/grit/node/__init__.py
 create mode 100644 tools/grit/grit/node/base.py
 create mode 100644 tools/grit/grit/node/base_unittest.py
 create mode 100644 tools/grit/grit/node/brotli_util.py
 create mode 100644 tools/grit/grit/node/custom/__init__.py
 create mode 100644 tools/grit/grit/node/custom/filename.py
 create mode 100644 tools/grit/grit/node/custom/filename_unittest.py
 create mode 100644 tools/grit/grit/node/empty.py
 create mode 100644 tools/grit/grit/node/include.py
 create mode 100644 tools/grit/grit/node/include_unittest.py
 create mode 100644 tools/grit/grit/node/mapping.py
 create mode 100644 tools/grit/grit/node/message.py
 create mode 100644 tools/grit/grit/node/message_unittest.py
 create mode 100644 tools/grit/grit/node/misc.py
 create mode 100644 tools/grit/grit/node/misc_unittest.py
 create mode 100644 tools/grit/grit/node/mock_brotli.py
 create mode 100644 tools/grit/grit/node/node_io.py
 create mode 100644 tools/grit/grit/node/node_io_unittest.py
 create mode 100644 tools/grit/grit/node/structure.py
 create mode 100644 tools/grit/grit/node/structure_unittest.py
 create mode 100644 tools/grit/grit/node/variant.py
 create mode 100644 tools/grit/grit/pseudo.py
 create mode 100644 tools/grit/grit/pseudo_rtl.py
 create mode 100644 tools/grit/grit/pseudo_unittest.py
 create mode 100644 tools/grit/grit/shortcuts.py
 create mode 100644 tools/grit/grit/shortcuts_unittest.py
 create mode 100644 tools/grit/grit/tclib.py
 create mode 100644 tools/grit/grit/tclib_unittest.py
 create mode 100644 tools/grit/grit/test_suite_all.py
 create mode 100644 tools/grit/grit/testdata/GoogleDesktop.adm
 create mode 100644 tools/grit/grit/testdata/README.txt
 create mode 100644 tools/grit/grit/testdata/about.html
 create mode 100644 tools/grit/grit/testdata/android.xml
 create mode 100644 tools/grit/grit/testdata/bad_browser.html
 create mode 100644 tools/grit/grit/testdata/browser.html
 create mode 100644 tools/grit/grit/testdata/buildinfo.grd
 create mode 100644 tools/grit/grit/testdata/cache_prefix.html
 create mode 100644 tools/grit/grit/testdata/cache_prefix_file.html
 create mode 100644 tools/grit/grit/testdata/chat_result.html
 create mode 100644 tools/grit/grit/testdata/chrome/app/generated_resources.grd
 create mode 100644 tools/grit/grit/testdata/chrome_html.html
 create mode 100644 tools/grit/grit/testdata/default_100_percent/a.png
 create mode 100644 tools/grit/grit/testdata/default_100_percent/b.png
 create mode 100644 tools/grit/grit/testdata/del_footer.html
 create mode 100644 tools/grit/grit/testdata/del_header.html
 create mode 100644 tools/grit/grit/testdata/deleted.html
 create mode 100644 tools/grit/grit/testdata/depfile.grd
 create mode 100644 tools/grit/grit/testdata/details.html
 create mode 100644 tools/grit/grit/testdata/duplicate-name-input.xml
 create mode 100644 tools/grit/grit/testdata/email_result.html
 create mode 100644 tools/grit/grit/testdata/email_thread.html
 create mode 100644 tools/grit/grit/testdata/error.html
 create mode 100644 tools/grit/grit/testdata/explicit_web.html
 create mode 100644 tools/grit/grit/testdata/footer.html
 create mode 100644 tools/grit/grit/testdata/generated_resources_fr.xtb
 create mode 100644 tools/grit/grit/testdata/generated_resources_iw.xtb
 create mode 100644 tools/grit/grit/testdata/generated_resources_no.xtb
 create mode 100644 tools/grit/grit/testdata/grit_part.grdp
 create mode 100644 tools/grit/grit/testdata/header.html
 create mode 100644 tools/grit/grit/testdata/homepage.html
 create mode 100644 tools/grit/grit/testdata/hover.html
 create mode 100644 tools/grit/grit/testdata/include_test.html
 create mode 100644 tools/grit/grit/testdata/included_sample.html
 create mode 100644 tools/grit/grit/testdata/indexing_speed.html
 create mode 100644 tools/grit/grit/testdata/install_prefs.html
 create mode 100644 tools/grit/grit/testdata/install_prefs2.html
 create mode 100644 tools/grit/grit/testdata/klonk-alternate-skeleton.rc
 create mode 100644 tools/grit/grit/testdata/klonk.ico
 create mode 100644 tools/grit/grit/testdata/klonk.rc
 create mode 100644 tools/grit/grit/testdata/ko_oem_enable_bug.html
 create mode 100644 tools/grit/grit/testdata/ko_oem_non_admin_bug.html
 create mode 100644 tools/grit/grit/testdata/mini.html
 create mode 100644 tools/grit/grit/testdata/oem_enable.html
 create mode 100644 tools/grit/grit/testdata/oem_non_admin.html
 create mode 100644 tools/grit/grit/testdata/onebox.html
 create mode 100644 tools/grit/grit/testdata/oneclick.html
 create mode 100644 tools/grit/grit/testdata/password.html
 create mode 100644 tools/grit/grit/testdata/preferences.html
 create mode 100644 tools/grit/grit/testdata/preprocess_test.html
 create mode 100644 tools/grit/grit/testdata/privacy.html
 create mode 100644 tools/grit/grit/testdata/quit_apps.html
 create mode 100644 tools/grit/grit/testdata/recrawl.html
 create mode 100644 tools/grit/grit/testdata/resource_ids
 create mode 100644 tools/grit/grit/testdata/script.html
 create mode 100644 tools/grit/grit/testdata/searchbox.html
 create mode 100644 tools/grit/grit/testdata/sidebar_h.html
 create mode 100644 tools/grit/grit/testdata/sidebar_v.html
 create mode 100644 tools/grit/grit/testdata/simple-input.xml
 create mode 100644 tools/grit/grit/testdata/simple.html
 create mode 100644 tools/grit/grit/testdata/source.rc
 create mode 100644 tools/grit/grit/testdata/special_100_percent/a.png
 create mode 100644 tools/grit/grit/testdata/status.html
 create mode 100644 tools/grit/grit/testdata/structure_variables.html
 create mode 100644 tools/grit/grit/testdata/substitute.grd
 create mode 100644 tools/grit/grit/testdata/substitute.xmb
 create mode 100644 tools/grit/grit/testdata/substitute_no_ids.grd
 create mode 100644 tools/grit/grit/testdata/substitute_tmpl.grd
 create mode 100644 tools/grit/grit/testdata/test_css.css
 create mode 100644 tools/grit/grit/testdata/test_html.html
 create mode 100644 tools/grit/grit/testdata/test_js.js
 create mode 100644 tools/grit/grit/testdata/test_svg.svg
 create mode 100644 tools/grit/grit/testdata/test_text.txt
 create mode 100644 tools/grit/grit/testdata/time_related.html
 create mode 100644 tools/grit/grit/testdata/toolbar_about.html
 create mode 100644 tools/grit/grit/testdata/tools/grit/resource_ids
 create mode 100644 tools/grit/grit/testdata/transl.rc
 create mode 100644 tools/grit/grit/testdata/versions.html
 create mode 100644 tools/grit/grit/testdata/whitelist.txt
 create mode 100644 tools/grit/grit/testdata/whitelist_resources.grd
 create mode 100644 tools/grit/grit/testdata/whitelist_strings.grd
 create mode 100644 tools/grit/grit/tool/__init__.py
 create mode 100644 tools/grit/grit/tool/android2grd.py
 create mode 100644 tools/grit/grit/tool/android2grd_unittest.py
 create mode 100644 tools/grit/grit/tool/build.py
 create mode 100644 tools/grit/grit/tool/build_unittest.py
 create mode 100644 tools/grit/grit/tool/buildinfo.py
 create mode 100644 tools/grit/grit/tool/buildinfo_unittest.py
 create mode 100644 tools/grit/grit/tool/count.py
 create mode 100644 tools/grit/grit/tool/diff_structures.py
 create mode 100644 tools/grit/grit/tool/diff_structures_unittest.py
 create mode 100644 tools/grit/grit/tool/interface.py
 create mode 100644 tools/grit/grit/tool/menu_from_parts.py
 create mode 100644 tools/grit/grit/tool/newgrd.py
 create mode 100644 tools/grit/grit/tool/newgrd_unittest.py
 create mode 100644 tools/grit/grit/tool/postprocess_interface.py
 create mode 100644 tools/grit/grit/tool/postprocess_unittest.py
 create mode 100644 tools/grit/grit/tool/preprocess_interface.py
 create mode 100644 tools/grit/grit/tool/preprocess_unittest.py
 create mode 100644 tools/grit/grit/tool/rc2grd.py
 create mode 100644 tools/grit/grit/tool/rc2grd_unittest.py
 create mode 100644 tools/grit/grit/tool/resize.py
 create mode 100644 tools/grit/grit/tool/test.py
 create mode 100644 tools/grit/grit/tool/transl2tc.py
 create mode 100644 tools/grit/grit/tool/transl2tc_unittest.py
 create mode 100644 tools/grit/grit/tool/unit.py
 create mode 100644 tools/grit/grit/tool/update_resource_ids/__init__.py
 create mode 100644 tools/grit/grit/tool/update_resource_ids/assigner.py
 create mode 100644 tools/grit/grit/tool/update_resource_ids/assigner_unittest.py
 create mode 100644 tools/grit/grit/tool/update_resource_ids/common.py
 create mode 100644 tools/grit/grit/tool/update_resource_ids/parser.py
 create mode 100644 tools/grit/grit/tool/update_resource_ids/reader.py
 create mode 100644 tools/grit/grit/tool/xmb.py
 create mode 100644 tools/grit/grit/tool/xmb_unittest.py
 create mode 100644 tools/grit/grit/util.py
 create mode 100644 tools/grit/grit/util_unittest.py
 create mode 100644 tools/grit/grit/xtb_reader.py
 create mode 100644 tools/grit/grit/xtb_reader_unittest.py
 create mode 100644 tools/grit/grit_info.py
 create mode 100644 tools/grit/grit_rule.gni
 create mode 100644 tools/grit/minify_with_uglify.py
 create mode 100644 tools/grit/minimize_css.py
 create mode 100644 tools/grit/minimize_css_unittest.py
 create mode 100644 tools/grit/pak_util.py
 create mode 100644 tools/grit/repack.gni
 create mode 100644 tools/grit/setup.py
 create mode 100644 tools/grit/stamp_grit_sources.py
 create mode 100644 tools/grit/third_party/six/LICENSE
 create mode 100644 tools/grit/third_party/six/README
 create mode 100644 tools/grit/third_party/six/README.chromium
 create mode 100644 tools/grit/third_party/six/__init__.py

diff --git a/tools/grit/.gitignore b/tools/grit/.gitignore
new file mode 100644
index 0000000000..0d20b6487c
--- /dev/null
+++ b/tools/grit/.gitignore
@@ -0,0 +1 @@
+*.pyc
diff --git a/tools/grit/BUILD.gn b/tools/grit/BUILD.gn
new file mode 100644
index 0000000000..1cd3c75b55
--- /dev/null
+++ b/tools/grit/BUILD.gn
@@ -0,0 +1,48 @@
+# Copyright 2014 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# This target creates a stamp file that depends on all the sources in the grit
+# directory. By depending on this, a target can force itself to be rebuilt if
+# grit itself changes.
+
+import("//build/config/sanitizers/sanitizers.gni")
+
+action("grit_sources") {
+  depfile = "$target_out_dir/grit_sources.d"
+  script = "stamp_grit_sources.py"
+
+  inputs = [ "grit.py" ]
+
+  # Note that we can't call this "grit_sources.stamp" because that file is
+  # implicitly created by GN for script actions.
+  outputs = [ "$target_out_dir/grit_sources.script.stamp" ]
+
+  args = [
+    rebase_path("//tools/grit", root_build_dir),
+    rebase_path(outputs[0], root_build_dir),
+    rebase_path(depfile, root_build_dir),
+  ]
+}
+
+group("grit_python_unittests") {
+  testonly = true
+
+  data = [
+    "//testing/scripts/common.py",
+    "//testing/scripts/run_isolated_script_test.py",
+    "//testing/xvfb.py",
+    "//tools/grit/",
+    "//third_party/catapult/third_party/typ/",
+  ]
+}
+
+# See https://crbug.com/983200
+if (is_mac && is_asan) {
+  create_bundle("brotli_mac_asan_workaround") {
+    bundle_root_dir = "$target_out_dir/$target_name"
+    bundle_executable_dir = bundle_root_dir
+
+    public_deps = [ "//third_party/brotli:brotli($host_toolchain)" ]
+  }
+}
diff --git a/tools/grit/MANIFEST.in b/tools/grit/MANIFEST.in
new file mode 100644
index 0000000000..1cbff42400
--- /dev/null
+++ b/tools/grit/MANIFEST.in
@@ -0,0 +1,3 @@
+exclude grit/test_suite_all.py
+exclude grit/tool/test.py
+global-exclude *_unittest.py
diff --git a/tools/grit/OWNERS b/tools/grit/OWNERS
new file mode 100644
index 0000000000..6a8f447b82
--- /dev/null
+++ b/tools/grit/OWNERS
@@ -0,0 +1,8 @@
+agrieve@chromium.org
+flackr@chromium.org
+thakis@chromium.org
+thestig@chromium.org
+
+# Admin policy related grit tools.
+per-file *policy*=file://components/policy/tools/OWNERS
+per-file *admin_template*=file://components/policy/tools/OWNERS
diff --git a/tools/grit/PRESUBMIT.py b/tools/grit/PRESUBMIT.py
new file mode 100644
index 0000000000..03b7188551
--- /dev/null
+++ b/tools/grit/PRESUBMIT.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""grit unittests presubmit script.
+
+See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts for
+details on the presubmit API built into gcl.
+"""
+
+
+def RunUnittests(input_api, output_api):
+  return input_api.canned_checks.RunUnitTests(input_api, output_api,
+      [input_api.os_path.join('grit', 'test_suite_all.py')])
+
+
+def CheckChangeOnUpload(input_api, output_api):
+  return RunUnittests(input_api, output_api)
+
+
+def CheckChangeOnCommit(input_api, output_api):
+  return RunUnittests(input_api, output_api)
diff --git a/tools/grit/README.md b/tools/grit/README.md
new file mode 100644
index 0000000000..b5c3f4b51b
--- /dev/null
+++ b/tools/grit/README.md
@@ -0,0 +1,19 @@
+# GRIT (Google Resource and Internationalization Tool)
+
+This is a tool for projects to manage resources and simplify the localization
+workflow.
+
+See the user guide for more details on using this project:
+https://dev.chromium.org/developers/tools-we-use-in-chromium/grit/grit-users-guide
+
+## History
+
+This code previously used to live at
+https://code.google.com/p/grit-i18n/source/checkout which still contains the
+project's history.  https://chromium.googlesource.com/external/grit-i18n/ is
+a git mirror of the SVN repository that's identical except for the last two
+commits.  The project is now developed in the Chromium project directly.
+
+There is a read-only mirror of just this directory at
+https://chromium.googlesource.com/chromium/src/tools/grit/ if you don't want to
+check out all of Chromium.
diff --git a/tools/grit/grit.py b/tools/grit/grit.py
new file mode 100644
index 0000000000..abd1ab6449
--- /dev/null
+++ b/tools/grit/grit.py
@@ -0,0 +1,31 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Bootstrapping for GRIT.
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+
+import grit.grit_runner
+
+sys.path.append(
+    os.path.join(
+        os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
+        'diagnosis'))
+try:
+  import crbug_1001171
+except ImportError:
+  crbug_1001171 = None
+
+
+if __name__ == '__main__':
+  if crbug_1001171:
+    with crbug_1001171.DumpStateOnLookupError():
+      sys.exit(grit.grit_runner.Main(sys.argv[1:]))
+  else:
+    sys.exit(grit.grit_runner.Main(sys.argv[1:]))
diff --git a/tools/grit/grit/__init__.py b/tools/grit/grit/__init__.py
new file mode 100644
index 0000000000..91ac9ee896
--- /dev/null
+++ b/tools/grit/grit/__init__.py
@@ -0,0 +1,19 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Package 'grit'
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+
+
+_CUR_DIR = os.path.abspath(os.path.dirname(__file__))
+_GRIT_DIR = os.path.dirname(_CUR_DIR)
+_THIRD_PARTY_DIR = os.path.join(_GRIT_DIR, 'third_party')
+
+if _THIRD_PARTY_DIR not in sys.path:
+  sys.path.insert(0, _THIRD_PARTY_DIR)
diff --git a/tools/grit/grit/clique.py b/tools/grit/grit/clique.py
new file mode 100644
index 0000000000..e7be3ec164
--- /dev/null
+++ b/tools/grit/grit/clique.py
@@ -0,0 +1,491 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Collections of messages and their translations, called cliques.  Also
+collections of cliques (uber-cliques).
+'''
+
+from __future__ import print_function
+
+import re
+
+import six
+
+from grit import constants
+from grit import exception
+from grit import lazy_re
+from grit import pseudo
+from grit import pseudo_rtl
+from grit import tclib
+
+
+class UberClique(object):
+  '''A factory (NOT a singleton factory) for making cliques.  It has several
+  methods for working with the cliques created using the factory.
+  '''
+
+  def __init__(self):
+    # A map from message ID to list of cliques whose source messages have
+    # that ID.  This will contain all cliques created using this factory.
+    # Different messages can have the same ID because they have the
+    # same translateable portion and placeholder names, but occur in different
+    # places in the resource tree.
+    #
+    # Each list of cliques is kept sorted by description, to achieve
+    # stable results from the BestClique method, see below.
+    self.cliques_ = {}
+
+    # A map of clique IDs to list of languages to indicate translations where we
+    # fell back to English.
+    self.fallback_translations_ = {}
+
+    # A map of clique IDs to list of languages to indicate missing translations.
+    self.missing_translations_ = {}
+
+  def _AddMissingTranslation(self, lang, clique, is_error):
+    tl = self.fallback_translations_
+    if is_error:
+      tl = self.missing_translations_
+    id = clique.GetId()
+    if id not in tl:
+      tl[id] = {}
+    if lang not in tl[id]:
+      tl[id][lang] = 1
+
+  def HasMissingTranslations(self):
+    return len(self.missing_translations_) > 0
+
+  def MissingTranslationsReport(self):
+    '''Returns a string suitable for printing to report missing
+    and fallback translations to the user.
+    '''
+    def ReportTranslation(clique, langs):
+      text = clique.GetMessage().GetPresentableContent()
+      # The text 'error' (usually 'Error:' but we are conservative)
+      # can trigger some build environments (Visual Studio, we're
+      # looking at you) to consider invocation of grit to have failed,
+      # so we make sure never to output that word.
+      extract = re.sub(r'(?i)error', 'REDACTED', text[0:40])[0:40]
+      ellipsis = ''
+      if len(text) > 40:
+        ellipsis = '...'
+      langs_extract = langs[0:6]
+      describe_langs = ','.join(langs_extract)
+      if len(langs) > 6:
+        describe_langs += " and %d more" % (len(langs) - 6)
+      return "  %s \"%s%s\" %s" % (clique.GetId(), extract, ellipsis,
+                                   describe_langs)
+    lines = []
+    if len(self.fallback_translations_):
+      lines.append(
+        "WARNING: Fell back to English for the following translations:")
+      for (id, langs) in self.fallback_translations_.items():
+        lines.append(
+            ReportTranslation(self.cliques_[id][0], list(langs.keys())))
+    if len(self.missing_translations_):
+      lines.append("ERROR: The following translations are MISSING:")
+      for (id, langs) in self.missing_translations_.items():
+        lines.append(
+            ReportTranslation(self.cliques_[id][0], list(langs.keys())))
+    return '\n'.join(lines)
+
+  def MakeClique(self, message, translateable=True):
+    '''Create a new clique initialized  with a message.
+
+    Args:
+      message: tclib.Message()
+      translateable: True | False
+    '''
+    clique = MessageClique(self, message, translateable)
+
+    # Enable others to find this clique by its message ID
+    if message.GetId() in self.cliques_:
+      presentable_text = clique.GetMessage().GetPresentableContent()
+      if not message.HasAssignedId():
+        for c in self.cliques_[message.GetId()]:
+          assert c.GetMessage().GetPresentableContent() == presentable_text
+      self.cliques_[message.GetId()].append(clique)
+      # We need to keep each list of cliques sorted by description, to
+      # achieve stable results from the BestClique method, see below.
+      self.cliques_[message.GetId()].sort(
+          key=lambda c:c.GetMessage().GetDescription())
+    else:
+      self.cliques_[message.GetId()] = [clique]
+
+    return clique
+
+  def FindCliqueAndAddTranslation(self, translation, language):
+    '''Adds the specified translation to the clique with the source message
+    it is a translation of.
+
+    Args:
+      translation: tclib.Translation()
+      language: 'en' | 'fr' ...
+
+    Return:
+      True if the source message was found, otherwise false.
+    '''
+    if translation.GetId() in self.cliques_:
+      for clique in self.cliques_[translation.GetId()]:
+        clique.AddTranslation(translation, language)
+      return True
+    else:
+      return False
+
+  def BestClique(self, id):
+    '''Returns the "best" clique from a list of cliques.  All the cliques
+    must have the same ID.  The "best" clique is chosen in the following
+    order of preference:
+    - The first clique that has a non-ID-based description.
+    - If no such clique found, the first clique with an ID-based description.
+    - Otherwise the first clique.
+
+    This method is stable in terms of always returning a clique with
+    an identical description (on different runs of GRIT on the same
+    data) because self.cliques_ is sorted by description.
+    '''
+    clique_list = self.cliques_[id]
+    clique_with_id = None
+    clique_default = None
+    for clique in clique_list:
+      if not clique_default:
+        clique_default = clique
+
+      description = clique.GetMessage().GetDescription()
+      if description and len(description) > 0:
+        if not description.startswith('ID:'):
+          # this is the preferred case so we exit right away
+          return clique
+        elif not clique_with_id:
+          clique_with_id = clique
+    if clique_with_id:
+      return clique_with_id
+    else:
+      return clique_default
+
+  def BestCliquePerId(self):
+    '''Iterates over the list of all cliques and returns the best clique for
+    each ID.  This will be the first clique with a source message that has a
+    non-empty description, or an arbitrary clique if none of them has a
+    description.
+    '''
+    for id in self.cliques_:
+      yield self.BestClique(id)
+
+  def BestCliqueByOriginalText(self, text, meaning):
+    '''Finds the "best" (as in BestClique()) clique that has original text
+    'text' and meaning 'meaning'.  Returns None if there is no such clique.
+    '''
+    # If needed, this can be optimized by maintaining a map of
+    # fingerprints of original text+meaning to cliques.
+    for c in self.BestCliquePerId():
+      msg = c.GetMessage()
+      if msg.GetRealContent() == text and msg.GetMeaning() == meaning:
+        return msg
+    return None
+
+  def AllMessageIds(self):
+    '''Returns a list of all defined message IDs.
+    '''
+    return list(self.cliques_.keys())
+
+  def AllCliques(self):
+    '''Iterates over all cliques.  Note that this can return multiple cliques
+    with the same ID.
+    '''
+    for cliques in self.cliques_.values():
+      for c in cliques:
+        yield c
+
+  def GenerateXtbParserCallback(self, lang, debug=False):
+    '''Creates a callback function as required by grit.xtb_reader.Parse().
+    This callback will create Translation objects for each message from
+    the XTB that exists in this uberclique, and add them as translations for
+    the relevant cliques.  The callback will add translations to the language
+    specified by 'lang'
+
+    Args:
+      lang: 'fr'
+      debug: True | False
+    '''
+    def Callback(id, structure):
+      if id not in self.cliques_:
+        if debug:
+          print("Ignoring translation #%s" % id)
+        return
+
+      if debug:
+        print("Adding translation #%s" % id)
+
+      # We fetch placeholder information from the original message (the XTB file
+      # only contains placeholder names).
+      original_msg = self.BestClique(id).GetMessage()
+
+      translation = tclib.Translation(id=id)
+      for is_ph,text in structure:
+        if not is_ph:
+          translation.AppendText(text)
+        else:
+          found_placeholder = False
+          for ph in original_msg.GetPlaceholders():
+            if ph.GetPresentation() == text:
+              translation.AppendPlaceholder(tclib.Placeholder(
+                ph.GetPresentation(), ph.GetOriginal(), ph.GetExample()))
+              found_placeholder = True
+              break
+          if not found_placeholder:
+            raise exception.MismatchingPlaceholders(
+              'Translation for message ID %s had <ph name="%s"/>, no match\n'
+              'in original message' % (id, text))
+      self.FindCliqueAndAddTranslation(translation, lang)
+    return Callback
+
+
+class CustomType(object):
+  '''A base class you should implement if you wish to specify a custom type
+  for a message clique (i.e. custom validation and optional modification of
+  translations).'''
+
+  def Validate(self, message):
+    '''Returns true if the message (a tclib.Message object) is valid,
+    otherwise false.
+    '''
+    raise NotImplementedError()
+
+  def ValidateAndModify(self, lang, translation):
+    '''Returns true if the translation (a tclib.Translation object) is valid,
+    otherwise false.  The language is also passed in.  This method may modify
+    the translation that is passed in, if it so wishes.
+    '''
+    raise NotImplementedError()
+
+  def ModifyTextPart(self, lang, text):
+    '''If you call ModifyEachTextPart, it will turn around and call this method
+    for each text part of the translation.  You should return the modified
+    version of the text, or just the original text to not change anything.
+    '''
+    raise NotImplementedError()
+
+  def ModifyEachTextPart(self, lang, translation):
+    '''Call this to easily modify one or more of the textual parts of a
+    translation.  It will call ModifyTextPart for each part of the
+    translation.
+    '''
+    contents = translation.GetContent()
+    for ix in range(len(contents)):
+      if (isinstance(contents[ix], six.string_types)):
+        contents[ix] = self.ModifyTextPart(lang, contents[ix])
+
+
+class OneOffCustomType(CustomType):
+  '''A very simple custom type that performs the validation expressed by
+  the input expression on all languages including the source language.
+  The expression can access the variables 'lang', 'msg' and 'text()' where
+  'lang' is the language of 'msg', 'msg' is the message or translation being
+  validated and 'text()' returns the real contents of 'msg' (for shorthand).
+  '''
+  def __init__(self, expression):
+    self.expr = expression
+  def Validate(self, message):
+    return self.ValidateAndModify(MessageClique.source_language, message)
+  def ValidateAndModify(self, lang, msg):
+    def text():
+      return msg.GetRealContent()
+    return eval(self.expr, {},
+            {'lang' : lang,
+             'text' : text,
+             'msg' : msg,
+             })
+
+
+class MessageClique(object):
+  '''A message along with all of its translations.  Also code to bring
+  translations together with their original message.'''
+
+  # change this to the language code of Messages you add to cliques_.
+  # TODO(joi) Actually change this based on the <grit> node's source language
+  source_language = 'en'
+
+  # A constant translation we use when asked for a translation into the
+  # special language constants.CONSTANT_LANGUAGE.
+  CONSTANT_TRANSLATION = tclib.Translation(text='TTTTTT')
+
+  # A pattern to match messages that are empty or whitespace only.
+  WHITESPACE_MESSAGE = lazy_re.compile(r'^\s*$')
+
+  def __init__(self, uber_clique, message, translateable=True,
+               custom_type=None):
+    '''Create a new clique initialized with just a message.
+
+    Note that messages with a body comprised only of whitespace will implicitly
+    be marked non-translatable.
+
+    Args:
+      uber_clique: Our uber-clique (collection of cliques)
+      message: tclib.Message()
+      translateable: True | False
+      custom_type: instance of clique.CustomType interface
+    '''
+    # Our parent
+    self.uber_clique = uber_clique
+    # If not translateable, we only store the original message.
+    self.translateable = translateable
+
+    # We implicitly mark messages that have a whitespace-only body as
+    # non-translateable.
+    if MessageClique.WHITESPACE_MESSAGE.match(message.GetRealContent()):
+      self.translateable = False
+
+    # A mapping of language identifiers to tclib.BaseMessage and its
+    # subclasses (i.e. tclib.Message and tclib.Translation).
+    self.clique = { MessageClique.source_language : message }
+    # A list of the "shortcut groups" this clique is
+    # part of.  Within any given shortcut group, no shortcut key (e.g. &J)
+    # must appear more than once in each language for all cliques that
+    # belong to the group.
+    self.shortcut_groups = []
+    # An instance of the CustomType interface, or None.  If this is set, it will
+    # be used to validate the original message and translations thereof, and
+    # will also get a chance to modify translations of the message.
+    self.SetCustomType(custom_type)
+
+  def GetMessage(self):
+    '''Retrieves the tclib.Message that is the source for this clique.'''
+    return self.clique[MessageClique.source_language]
+
+  def GetId(self):
+    '''Retrieves the message ID of the messages in this clique.'''
+    return self.GetMessage().GetId()
+
+  def IsTranslateable(self):
+    return self.translateable
+
+  def AddToShortcutGroup(self, group):
+    self.shortcut_groups.append(group)
+
+  def SetCustomType(self, custom_type):
+    '''Makes this clique use custom_type for validating messages and
+    translations, and optionally modifying translations.
+    '''
+    self.custom_type = custom_type
+    if custom_type and not custom_type.Validate(self.GetMessage()):
+      raise exception.InvalidMessage(self.GetMessage().GetRealContent())
+
+  def MessageForLanguage(self, lang, pseudo_if_no_match=True,
+                         fallback_to_english=False):
+    '''Returns the message/translation for the specified language, providing
+    a pseudotranslation if there is no available translation and a pseudo-
+    translation is requested.
+
+    The translation of any message whatsoever in the special language
+    'x_constant' is the message "TTTTTT".
+
+    Args:
+      lang: 'en'
+      pseudo_if_no_match: True
+      fallback_to_english: False
+
+    Return:
+      tclib.BaseMessage
+    '''
+    if not self.translateable:
+      return self.GetMessage()
+
+    if lang == constants.CONSTANT_LANGUAGE:
+      return self.CONSTANT_TRANSLATION
+
+    for msglang in self.clique:
+      if lang == msglang:
+        return self.clique[msglang]
+
+    if lang == constants.FAKE_BIDI:
+      return pseudo_rtl.PseudoRTLMessage(self.GetMessage())
+
+    if fallback_to_english:
+      self.uber_clique._AddMissingTranslation(lang, self, is_error=False)
+      return self.GetMessage()
+
+    # If we're not supposed to generate pseudotranslations, we add an error
+    # report to a list of errors, then fail at a higher level, so that we
+    # get a list of all messages that are missing translations.
+    if not pseudo_if_no_match:
+      self.uber_clique._AddMissingTranslation(lang, self, is_error=True)
+
+    return pseudo.PseudoMessage(self.GetMessage())
+
+  def AllMessagesThatMatch(self, lang_re, include_pseudo = True):
+    '''Returns a map of all messages that match 'lang', including the pseudo
+    translation if requested.
+
+    Args:
+      lang_re: re.compile(r'fr|en')
+      include_pseudo: True
+
+    Return:
+      { 'en' : tclib.Message,
+        'fr' : tclib.Translation,
+        pseudo.PSEUDO_LANG : tclib.Translation }
+    '''
+    if not self.translateable:
+      return [self.GetMessage()]
+
+    matches = {}
+    for msglang in self.clique:
+      if lang_re.match(msglang):
+        matches[msglang] = self.clique[msglang]
+
+    if include_pseudo:
+      matches[pseudo.PSEUDO_LANG] = pseudo.PseudoMessage(self.GetMessage())
+
+    return matches
+
+  def AddTranslation(self, translation, language):
+    '''Add a translation to this clique.  The translation must have the same
+    ID as the message that is the source for this clique.
+
+    If this clique is not translateable, the function just returns.
+
+    Args:
+      translation: tclib.Translation()
+      language: 'en'
+
+    Throws:
+      grit.exception.InvalidTranslation if the translation you're trying to add
+      doesn't have the same message ID as the source message of this clique.
+    '''
+    if not self.translateable:
+      return
+    if translation.GetId() != self.GetId():
+      raise exception.InvalidTranslation(
+        'Msg ID %s, transl ID %s' % (self.GetId(), translation.GetId()))
+
+    assert not language in self.clique
+
+    # Because two messages can differ in the original content of their
+    # placeholders yet share the same ID (because they are otherwise the
+    # same), the translation we are getting may have different original
+    # content for placeholders than our message, yet it is still the right
+    # translation for our message (because it is for the same ID).  We must
+    # therefore fetch the original content of placeholders from our original
+    # English message.
+    #
+    # See grit.clique_unittest.MessageCliqueUnittest.testSemiIdenticalCliques
+    # for a concrete explanation of why this is necessary.
+
+    original = self.MessageForLanguage(self.source_language, False)
+    if len(original.GetPlaceholders()) != len(translation.GetPlaceholders()):
+      print("ERROR: '%s' translation of message id %s does not match" %
+            (language, translation.GetId()))
+      assert False
+
+    transl_msg = tclib.Translation(id=self.GetId(),
+                                   text=translation.GetPresentableContent(),
+                                   placeholders=original.GetPlaceholders())
+
+    if (self.custom_type and
+        not self.custom_type.ValidateAndModify(language, transl_msg)):
+      print("WARNING: %s translation failed validation: %s" %
+            (language, transl_msg.GetId()))
+
+    self.clique[language] = transl_msg
diff --git a/tools/grit/grit/clique_unittest.py b/tools/grit/grit/clique_unittest.py
new file mode 100644
index 0000000000..7d2d7318ba
--- /dev/null
+++ b/tools/grit/grit/clique_unittest.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.clique'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import re
+import unittest
+
+from six import StringIO
+
+from grit import clique
+from grit import exception
+from grit import pseudo
+from grit import tclib
+from grit import grd_reader
+from grit import util
+
+class MessageCliqueUnittest(unittest.TestCase):
+  def testClique(self):
+    factory = clique.UberClique()
+    msg = tclib.Message(text='Hello USERNAME, how are you?',
+                        placeholders=[
+                          tclib.Placeholder('USERNAME', '%s', 'Joi')])
+    c = factory.MakeClique(msg)
+
+    self.failUnless(c.GetMessage() == msg)
+    self.failUnless(c.GetId() == msg.GetId())
+
+    msg_fr = tclib.Translation(text='Bonjour USERNAME, comment ca va?',
+                               id=msg.GetId(), placeholders=[
+                                tclib.Placeholder('USERNAME', '%s', 'Joi')])
+    msg_de = tclib.Translation(text='Guten tag USERNAME, wie geht es dir?',
+                               id=msg.GetId(), placeholders=[
+                                tclib.Placeholder('USERNAME', '%s', 'Joi')])
+
+    c.AddTranslation(msg_fr, 'fr')
+    factory.FindCliqueAndAddTranslation(msg_de, 'de')
+
+    # sort() sorts lists in-place and does not return them
+    for lang in ('en', 'fr', 'de'):
+      self.failUnless(lang in c.clique)
+
+    self.failUnless(c.MessageForLanguage('fr').GetRealContent() ==
+                    msg_fr.GetRealContent())
+
+    try:
+      c.MessageForLanguage('zh-CN', False)
+      self.fail('Should have gotten exception')
+    except:
+      pass
+
+    self.failUnless(c.MessageForLanguage('zh-CN', True) != None)
+
+    rex = re.compile('fr|de|bingo')
+    self.failUnless(len(c.AllMessagesThatMatch(rex, False)) == 2)
+    self.failUnless(
+        c.AllMessagesThatMatch(rex, True)[pseudo.PSEUDO_LANG] is not None)
+
+  def testBestClique(self):
+    factory = clique.UberClique()
+    factory.MakeClique(tclib.Message(text='Alfur', description='alfaholl'))
+    factory.MakeClique(tclib.Message(text='Alfur', description=''))
+    factory.MakeClique(tclib.Message(text='Vaettur', description=''))
+    factory.MakeClique(tclib.Message(text='Vaettur', description=''))
+    factory.MakeClique(tclib.Message(text='Troll', description=''))
+    factory.MakeClique(tclib.Message(text='Gryla', description='ID: IDS_GRYLA'))
+    factory.MakeClique(tclib.Message(text='Gryla', description='vondakerling'))
+    factory.MakeClique(tclib.Message(text='Leppaludi', description='ID: IDS_LL'))
+    factory.MakeClique(tclib.Message(text='Leppaludi', description=''))
+
+    count_best_cliques = 0
+    for c in factory.BestCliquePerId():
+      count_best_cliques += 1
+      msg = c.GetMessage()
+      text = msg.GetRealContent()
+      description = msg.GetDescription()
+      if text == 'Alfur':
+        self.failUnless(description == 'alfaholl')
+      elif text == 'Gryla':
+        self.failUnless(description == 'vondakerling')
+      elif text == 'Leppaludi':
+        self.failUnless(description == 'ID: IDS_LL')
+    self.failUnless(count_best_cliques == 5)
+
+  def testAllInUberClique(self):
+    resources = grd_reader.Parse(
+        StringIO(u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+  <release seq="3">
+    <messages>
+      <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+        Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+      </message>
+    </messages>
+    <structures>
+      <structure type="dialog" name="IDD_ABOUTBOX" encoding="utf-16" file="grit/testdata/klonk.rc" />
+      <structure type="tr_html" name="ID_HTML" file="grit/testdata/simple.html" />
+    </structures>
+  </release>
+</grit>'''), util.PathFromRoot('.'))
+    resources.SetOutputLanguage('en')
+    resources.RunGatherers()
+    content_list = []
+    for clique_list in resources.UberClique().cliques_.values():
+      for clique in clique_list:
+        content_list.append(clique.GetMessage().GetRealContent())
+    self.failUnless('Hello %s, how are you doing today?' in content_list)
+    self.failUnless('Jack "Black" Daniels' in content_list)
+    self.failUnless('Hello!' in content_list)
+
+  def testCorrectExceptionIfWrongEncodingOnResourceFile(self):
+    '''This doesn't really belong in this unittest file, but what the heck.'''
+    resources = grd_reader.Parse(
+        StringIO(u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+  <release seq="3">
+    <structures>
+      <structure type="dialog" name="IDD_ABOUTBOX" file="grit/testdata/klonk.rc" />
+    </structures>
+  </release>
+</grit>'''), util.PathFromRoot('.'))
+    self.assertRaises(exception.SectionNotFound, resources.RunGatherers)
+
+  def testSemiIdenticalCliques(self):
+    messages = [
+      tclib.Message(text='Hello USERNAME',
+                    placeholders=[tclib.Placeholder('USERNAME', '$1', 'Joi')]),
+      tclib.Message(text='Hello USERNAME',
+                    placeholders=[tclib.Placeholder('USERNAME', '%s', 'Joi')]),
+    ]
+    self.failUnless(messages[0].GetId() == messages[1].GetId())
+
+    # Both of the above would share a translation.
+    translation = tclib.Translation(id=messages[0].GetId(),
+                                    text='Bonjour USERNAME',
+                                    placeholders=[tclib.Placeholder(
+                                      'USERNAME', '$1', 'Joi')])
+
+    factory = clique.UberClique()
+    cliques = [factory.MakeClique(msg) for msg in messages]
+
+    for clq in cliques:
+      clq.AddTranslation(translation, 'fr')
+
+    self.failUnless(cliques[0].MessageForLanguage('fr').GetRealContent() ==
+                    'Bonjour $1')
+    self.failUnless(cliques[1].MessageForLanguage('fr').GetRealContent() ==
+                    'Bonjour %s')
+
+  def testMissingTranslations(self):
+    messages = [ tclib.Message(text='Hello'), tclib.Message(text='Goodbye') ]
+    factory = clique.UberClique()
+    cliques = [factory.MakeClique(msg) for msg in messages]
+
+    cliques[1].MessageForLanguage('fr', False, True)
+
+    self.failUnless(not factory.HasMissingTranslations())
+
+    cliques[0].MessageForLanguage('de', False, False)
+
+    self.failUnless(factory.HasMissingTranslations())
+
+    report = factory.MissingTranslationsReport()
+    self.failUnless(report.count('WARNING') == 1)
+    self.failUnless(report.count('8053599568341804890 "Goodbye" fr') == 1)
+    self.failUnless(report.count('ERROR') == 1)
+    self.failUnless(report.count('800120468867715734 "Hello" de') == 1)
+
+  def testCustomTypes(self):
+    factory = clique.UberClique()
+    message = tclib.Message(text='Bingo bongo')
+    c = factory.MakeClique(message)
+    try:
+      c.SetCustomType(DummyCustomType())
+      self.fail()
+    except:
+      pass  # expected case - 'Bingo bongo' does not start with 'jjj'
+
+    message = tclib.Message(text='jjjBingo bongo')
+    c = factory.MakeClique(message)
+    c.SetCustomType(util.NewClassInstance(
+      'grit.clique_unittest.DummyCustomType', clique.CustomType))
+    translation = tclib.Translation(id=message.GetId(), text='Bilingo bolongo')
+    c.AddTranslation(translation, 'fr')
+    self.failUnless(c.MessageForLanguage('fr').GetRealContent().startswith('jjj'))
+
+  def testWhitespaceMessagesAreNontranslateable(self):
+    factory = clique.UberClique()
+
+    message = tclib.Message(text=' \t')
+    c = factory.MakeClique(message, translateable=True)
+    self.failIf(c.IsTranslateable())
+
+    message = tclib.Message(text='\n \n ')
+    c = factory.MakeClique(message, translateable=True)
+    self.failIf(c.IsTranslateable())
+
+    message = tclib.Message(text='\n hello')
+    c = factory.MakeClique(message, translateable=True)
+    self.failUnless(c.IsTranslateable())
+
+  def testEachCliqueKeptSorted(self):
+    factory = clique.UberClique()
+    msg_a = tclib.Message(text='hello', description='a')
+    msg_b = tclib.Message(text='hello', description='b')
+    msg_c = tclib.Message(text='hello', description='c')
+    # Insert out of order
+    clique_b = factory.MakeClique(msg_b, translateable=True)
+    clique_a = factory.MakeClique(msg_a, translateable=True)
+    clique_c = factory.MakeClique(msg_c, translateable=True)
+    clique_list = factory.cliques_[clique_a.GetId()]
+    self.failUnless(len(clique_list) == 3)
+    self.failUnless(clique_list[0] == clique_a)
+    self.failUnless(clique_list[1] == clique_b)
+    self.failUnless(clique_list[2] == clique_c)
+
+  def testBestCliqueSortIsStable(self):
+    factory = clique.UberClique()
+    text = 'hello'
+    msg_no_description = tclib.Message(text=text)
+    msg_id_description_a = tclib.Message(text=text, description='ID: a')
+    msg_id_description_b = tclib.Message(text=text, description='ID: b')
+    msg_description_x = tclib.Message(text=text, description='x')
+    msg_description_y = tclib.Message(text=text, description='y')
+    clique_id = msg_no_description.GetId()
+
+    # Insert in an order that tests all outcomes.
+    clique_no_description = factory.MakeClique(msg_no_description,
+                                               translateable=True)
+    self.failUnless(factory.BestClique(clique_id) == clique_no_description)
+    clique_id_description_b = factory.MakeClique(msg_id_description_b,
+                                                 translateable=True)
+    self.failUnless(factory.BestClique(clique_id) == clique_id_description_b)
+    clique_id_description_a = factory.MakeClique(msg_id_description_a,
+                                                 translateable=True)
+    self.failUnless(factory.BestClique(clique_id) == clique_id_description_a)
+    clique_description_y = factory.MakeClique(msg_description_y,
+                                              translateable=True)
+    self.failUnless(factory.BestClique(clique_id) == clique_description_y)
+    clique_description_x = factory.MakeClique(msg_description_x,
+                                              translateable=True)
+    self.failUnless(factory.BestClique(clique_id) == clique_description_x)
+
+
+class DummyCustomType(clique.CustomType):
+  def Validate(self, message):
+    return message.GetRealContent().startswith('jjj')
+  def ValidateAndModify(self, lang, translation):
+    is_ok = self.Validate(translation)
+    self.ModifyEachTextPart(lang, translation)
+  def ModifyTextPart(self, lang, text):
+    return 'jjj%s' % text
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/constants.py b/tools/grit/grit/constants.py
new file mode 100644
index 0000000000..8229c94b09
--- /dev/null
+++ b/tools/grit/grit/constants.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Constant definitions for GRIT.
+'''
+
+from __future__ import print_function
+
+# This is the Icelandic noun meaning "grit" and is used to check that our
+# input files are in the correct encoding.  The middle character gets encoded
+# as two bytes in UTF-8, so this is sufficient to detect incorrect encoding.
+ENCODING_CHECK = u'm\u00f6l'
+
+# A special language, translations into which are always "TTTTTT".
+CONSTANT_LANGUAGE = 'x_constant'
+
+FAKE_BIDI = 'fake-bidi'
+
+# Magic number added to the header of resources brotli compressed by grit. Used
+# to easily identify resources as being brotli compressed. See
+# ui/base/resource/resource_bundle.h for decompression usage.
+BROTLI_CONST = b'\x1e\x9b'
diff --git a/tools/grit/grit/exception.py b/tools/grit/grit/exception.py
new file mode 100644
index 0000000000..2a363fb077
--- /dev/null
+++ b/tools/grit/grit/exception.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Exception types for GRIT.
+'''
+
+from __future__ import print_function
+
+class Base(Exception):
+  '''A base exception that uses the class's docstring in addition to any
+  user-provided message as the body of the Base.
+  '''
+  def __init__(self, msg=''):
+    if len(msg):
+      if self.__doc__:
+        msg = self.__doc__ + ': ' + msg
+    else:
+      msg = self.__doc__
+    super(Base, self).__init__(msg)
+
+
+class Parsing(Base):
+  '''An error occurred parsing a GRD or XTB file.'''
+  pass
+
+
+class UnknownElement(Parsing):
+  '''An unknown node type was encountered.'''
+  pass
+
+
+class MissingElement(Parsing):
+  '''An expected element was missing.'''
+  pass
+
+
+class UnexpectedChild(Parsing):
+  '''An unexpected child element was encountered (on a leaf node).'''
+  pass
+
+
+class UnexpectedAttribute(Parsing):
+  '''The attribute was not expected'''
+  pass
+
+
+class UnexpectedContent(Parsing):
+  '''This element should not have content'''
+  pass
+
+class MissingMandatoryAttribute(Parsing):
+  '''This element is missing a mandatory attribute'''
+  pass
+
+
+class MutuallyExclusiveMandatoryAttribute(Parsing):
+  '''This element has 2 mutually exclusive mandatory attributes'''
+  pass
+
+
+class DuplicateKey(Parsing):
+  '''A duplicate key attribute was found.'''
+  pass
+
+
+class TooManyExamples(Parsing):
+  '''Only one <ex> element is allowed for each <ph> element.'''
+  pass
+
+
+class FileNotFound(Parsing):
+  '''The resource file was not found.'''
+  pass
+
+
+class InvalidMessage(Base):
+  '''The specified message failed validation.'''
+  pass
+
+
+class InvalidTranslation(Base):
+  '''Attempt to add an invalid translation to a clique.'''
+  pass
+
+
+class NoSuchTranslation(Base):
+  '''Requested translation not available'''
+  pass
+
+
+class NotReady(Base):
+  '''Attempt to use an object before it is ready, or attempt to translate \
+an empty document.'''
+  pass
+
+
+class MismatchingPlaceholders(Base):
+  '''Placeholders do not match.'''
+  pass
+
+
+class InvalidPlaceholderName(Base):
+  '''Placeholder name can only contain A-Z, a-z, 0-9 and underscore.'''
+  pass
+
+
+class BlockTagInTranslateableChunk(Base):
+  '''A block tag was encountered where it wasn't expected.'''
+  pass
+
+
+class SectionNotFound(Base):
+  '''The section you requested was not found in the RC file. Make \
+sure the section ID is correct (matches the section's ID in the RC file). \
+Also note that you may need to specify the RC file's encoding (using the \
+encoding="" attribute) if it is not in the default Windows-1252 encoding. \
+'''
+  pass
+
+
+class IdRangeOverlap(Base):
+  '''ID range overlap.'''
+  pass
+
+
+class ReservedHeaderCollision(Base):
+  '''Resource included with first 3 bytes matching reserved header.'''
+  pass
+
+
+class PlaceholderNotInsidePhNode(Base):
+  '''Placeholder formatters should be inside <ph> element.'''
+  pass
+
+
+class InvalidCharactersInsidePhNode(Base):
+  '''Invalid characters found inside <ph> element.'''
+  pass
diff --git a/tools/grit/grit/extern/BogoFP.py b/tools/grit/grit/extern/BogoFP.py
new file mode 100644
index 0000000000..fc90145833
--- /dev/null
+++ b/tools/grit/grit/extern/BogoFP.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Bogus fingerprint implementation, do not use for production,
+provided only as an example.
+
+Usage:
+    grit.py -h grit.extern.BogoFP xmb /tmp/foo
+"""
+
+from __future__ import print_function
+
+import grit.extern.FP
+
+
+def UnsignedFingerPrint(str, encoding='utf-8'):
+  """Generate a fingerprint not intended for production from str (it
+  reduces the precision of the production fingerprint by one bit).
+  """
+  return (0xFFFFF7FFFFFFFFFF &
+          grit.extern.FP._UnsignedFingerPrintImpl(str, encoding))
diff --git a/tools/grit/grit/extern/FP.py b/tools/grit/grit/extern/FP.py
new file mode 100644
index 0000000000..f4ec4d943f
--- /dev/null
+++ b/tools/grit/grit/extern/FP.py
@@ -0,0 +1,72 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import print_function
+
+try:
+  import hashlib
+  _new_md5 = hashlib.md5
+except ImportError:
+  import md5
+  _new_md5 = md5.new
+
+
+"""64-bit fingerprint support for strings.
+
+Usage:
+    from extern import FP
+    print('Fingerprint is %ld' % FP.FingerPrint('Hello world!'))
+"""
+
+
+def _UnsignedFingerPrintImpl(str, encoding='utf-8'):
+  """Generate a 64-bit fingerprint by taking the first half of the md5
+  of the string.
+  """
+  hex128 = _new_md5(str.encode(encoding)).hexdigest()
+  int64 = int(hex128[:16], 16)
+  return int64
+
+
+def UnsignedFingerPrint(str, encoding='utf-8'):
+  """Generate a 64-bit fingerprint.
+
+  The default implementation uses _UnsignedFingerPrintImpl, which
+  takes the first half of the md5 of the string, but the
+  implementation may be switched using SetUnsignedFingerPrintImpl.
+  """
+  return _UnsignedFingerPrintImpl(str, encoding)
+
+
+def FingerPrint(str, encoding='utf-8'):
+  fp = UnsignedFingerPrint(str, encoding=encoding)
+  # interpret fingerprint as signed longs
+  if fp & 0x8000000000000000:
+    fp = -((~fp & 0xFFFFFFFFFFFFFFFF) + 1)
+  return fp
+
+
+def UseUnsignedFingerPrintFromModule(module_name):
+  """Imports module_name and replaces UnsignedFingerPrint in the
+  current module with the function of the same name from the imported
+  module.
+
+  Returns the function object previously known as
+  grit.extern.FP.UnsignedFingerPrint.
+  """
+  hash_module = __import__(module_name, fromlist=[module_name])
+  return SetUnsignedFingerPrint(hash_module.UnsignedFingerPrint)
+
+
+def SetUnsignedFingerPrint(function_object):
+  """Sets grit.extern.FP.UnsignedFingerPrint to point to
+  function_object.
+
+  Returns the function object previously known as
+  grit.extern.FP.UnsignedFingerPrint.
+  """
+  global UnsignedFingerPrint
+  original_function_object = UnsignedFingerPrint
+  UnsignedFingerPrint = function_object
+  return original_function_object
diff --git a/tools/grit/grit/extern/__init__.py b/tools/grit/grit/extern/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/tools/grit/grit/extern/tclib.py b/tools/grit/grit/extern/tclib.py
new file mode 100644
index 0000000000..9952a87c11
--- /dev/null
+++ b/tools/grit/grit/extern/tclib.py
@@ -0,0 +1,503 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# The tclib module contains tools for aggregating, verifying, and storing
+# messages destined for the Translation Console, as well as for reading
+# translations back and outputting them in some desired format.
+#
+# This has been stripped down to include only the functionality needed by grit
+# for creating Windows .rc and .h files.  These are the only parts needed by
+# the Chrome build process.
+
+from __future__ import print_function
+
+from grit.extern import FP
+
+# This module assumes that within a bundle no two messages can have the
+# same id unless they're identical.
+
+# The basic classes defined here for external use are Message and Translation,
+# where the former is used for English messages and the latter for
+# translations. These classes have a lot of common functionality, as expressed
+# by the common parent class BaseMessage. Perhaps the most important
+# distinction is that translated text is stored in UTF-8, whereas original text
+# is stored in whatever encoding the client uses (presumably Latin-1).
+
+# --------------------
+# The public interface
+# --------------------
+
+# Generate message id from message text and meaning string (optional),
+# both in utf-8 encoding
+#
+def GenerateMessageId(message, meaning=''):
+  fp = FP.FingerPrint(message)
+  if meaning:
+    # combine the fingerprints of message and meaning
+    fp2 = FP.FingerPrint(meaning)
+    if fp < 0:
+      fp = fp2 + (fp << 1) + 1
+    else:
+      fp = fp2 + (fp << 1)
+  # To avoid negative ids we strip the high-order bit
+  return str(fp & 0x7fffffffffffffff)
+
+# -------------------------------------------------------------------------
+# The MessageTranslationError class is used to signal tclib-specific errors.
+
+
+class MessageTranslationError(Exception):
+
+  def __init__(self, args = ''):
+    self.args = args
+
+
+# -----------------------------------------------------------
+# The Placeholder class represents a placeholder in a message.
+
+class Placeholder(object):
+  # String representation
+  def __str__(self):
+    return '%s, "%s", "%s"' % \
+           (self.__presentation, self.__original, self.__example)
+
+  # Getters
+  def GetOriginal(self):
+    return self.__original
+
+  def GetPresentation(self):
+    return self.__presentation
+
+  def GetExample(self):
+    return self.__example
+
+  def __eq__(self, other):
+    return self.EqualTo(other, strict=1, ignore_trailing_spaces=0)
+
+  # Equality test
+  #
+  # ignore_trailing_spaces: TC is using varchar to store the
+  # phrwr fields, as a result of that, the trailing spaces
+  # are removed by MySQL when the strings are stored into TC:-(
+  # ignore_trailing_spaces parameter is used to ignore
+  # trailing spaces during equivalence comparison.
+  #
+  def EqualTo(self, other, strict = 1, ignore_trailing_spaces = 1):
+    if type(other) is not Placeholder:
+      return 0
+    if StringEquals(self.__presentation, other.__presentation,
+                    ignore_trailing_spaces):
+      if not strict or (StringEquals(self.__original, other.__original,
+                                     ignore_trailing_spaces)  and
+                        StringEquals(self.__example, other.__example,
+                                     ignore_trailing_spaces)):
+        return 1
+    return 0
+
+
+# -----------------------------------------------------------------
+# BaseMessage is the common parent class of Message and Translation.
+# It is not meant for direct use.
+
+class BaseMessage(object):
+  # Three types of message construction is supported. If the message text is a
+  # simple string with no dynamic content, you can pass it to the constructor
+  # as the "text" parameter. Otherwise, you can omit "text" and assemble the
+  # message step by step using AppendText() and AppendPlaceholder(). Or, as an
+  # alternative, you can give the constructor the "presentable" version of the
+  # message and a list of placeholders; it will then parse the presentation and
+  # build the message accordingly. For example:
+  # Message(text = "There are NUM_BUGS bugs in your code",
+  #         placeholders = [Placeholder("NUM_BUGS", "%d", "33")],
+  #         description = "Bla bla bla")
+  def __eq__(self, other):
+    # "source encoding" is nonsense, so ignore it
+    return _ObjectEquals(self, other, ['_BaseMessage__source_encoding'])
+
+  def GetName(self):
+    return self.__name
+
+  def GetSourceEncoding(self):
+    return self.__source_encoding
+
+  # Append a placeholder to the message
+  def AppendPlaceholder(self, placeholder):
+    if not isinstance(placeholder, Placeholder):
+      raise MessageTranslationError("Invalid message placeholder %s in "
+                                    "message %s" % (placeholder, self.GetId()))
+    # Are there other placeholders with the same presentation?
+    # If so, they need to be the same.
+    for other in self.GetPlaceholders():
+      if placeholder.GetPresentation() == other.GetPresentation():
+        if not placeholder.EqualTo(other):
+          raise MessageTranslationError(
+              "Conflicting declarations of %s within message" %
+              placeholder.GetPresentation())
+    # update placeholder list
+    dup = 0
+    for item in self.__content:
+      if isinstance(item, Placeholder) and placeholder.EqualTo(item):
+        dup = 1
+        break
+    if not dup:
+      self.__placeholders.append(placeholder)
+
+    # update content
+    self.__content.append(placeholder)
+
+  # Strips leading and trailing whitespace, and returns a tuple
+  # containing the leading and trailing space that was removed.
+  def Strip(self):
+    leading = trailing = ''
+    if len(self.__content) > 0:
+      s0 = self.__content[0]
+      if not isinstance(s0, Placeholder):
+        s = s0.lstrip()
+        leading = s0[:-len(s)]
+        self.__content[0] = s
+
+      s0 = self.__content[-1]
+      if not isinstance(s0, Placeholder):
+        s = s0.rstrip()
+        trailing = s0[len(s):]
+        self.__content[-1] = s
+    return leading, trailing
+
+  # Return the id of this message
+  def GetId(self):
+    if self.__id is None:
+      return self.GenerateId()
+    return self.__id
+
+  # Set the id of this message
+  def SetId(self, id):
+    if id is None:
+      self.__id = None
+    else:
+      self.__id = str(id)  # Treat numerical ids as strings
+
+  # Return content of this message as a list (internal use only)
+  def GetContent(self):
+    return self.__content
+
+  # Return a human-readable version of this message
+  def GetPresentableContent(self):
+    presentable_content = ""
+    for item in self.__content:
+      if isinstance(item, Placeholder):
+        presentable_content += item.GetPresentation()
+      else:
+        presentable_content += item
+
+    return presentable_content
+
+  # Return a fragment of a message in escaped format
+  def EscapeFragment(self, fragment):
+    return fragment.replace('%', '%%')
+
+  # Return the "original" version of this message, doing %-escaping
+  # properly.  If source_msg is specified, the placeholder original
+  # information inside source_msg will be used instead.
+  def GetOriginalContent(self, source_msg = None):
+    original_content = ""
+    for item in self.__content:
+      if isinstance(item, Placeholder):
+        if source_msg:
+          ph = source_msg.GetPlaceholder(item.GetPresentation())
+          if not ph:
+            raise MessageTranslationError(
+                "Placeholder %s doesn't exist in message: %s" %
+                (item.GetPresentation(), source_msg))
+          original_content += ph.GetOriginal()
+        else:
+          original_content += item.GetOriginal()
+      else:
+        original_content += self.EscapeFragment(item)
+    return original_content
+
+  # Return the example of this message
+  def GetExampleContent(self):
+    example_content = ""
+    for item in self.__content:
+      if isinstance(item, Placeholder):
+        example_content += item.GetExample()
+      else:
+        example_content += item
+    return example_content
+
+  # Return a list of all unique placeholders in this message
+  def GetPlaceholders(self):
+    return self.__placeholders
+
+  # Return a placeholder in this message
+  def GetPlaceholder(self, presentation):
+    for item in self.__content:
+      if (isinstance(item, Placeholder) and
+          item.GetPresentation() == presentation):
+        return item
+    return None
+
+  # Return this message's description
+  def GetDescription(self):
+    return self.__description
+
+  # Add a message source
+  def AddSource(self, source):
+    self.__sources.append(source)
+
+  # Return this message's sources as a list
+  def GetSources(self):
+    return self.__sources
+
+  # Return this message's sources as a string
+  def GetSourcesAsText(self, delimiter = "; "):
+    return delimiter.join(self.__sources)
+
+  # Set the obsolete flag for a message (internal use only)
+  def SetObsolete(self):
+    self.__obsolete = 1
+
+  # Get the obsolete flag for a message (internal use only)
+  def IsObsolete(self):
+    return self.__obsolete
+
+  # Get the sequence number (0 by default)
+  def GetSequenceNumber(self):
+    return self.__sequence_number
+
+  # Set the sequence number
+  def SetSequenceNumber(self, number):
+    self.__sequence_number = number
+
+  # Increment instance counter
+  def AddInstance(self):
+    self.__num_instances += 1
+
+  # Return instance count
+  def GetNumInstances(self):
+    return self.__num_instances
+
+  def GetErrors(self, from_tc=0):
+    """
+    Returns a description of the problem if the message is not
+    syntactically valid, or None if everything is fine.
+
+    Args:
+      from_tc: indicates whether this message came from the TC. We let
+      the TC get away with some things we normally wouldn't allow for
+      historical reasons.
+    """
+    # check that placeholders are unambiguous
+    pos = 0
+    phs = {}
+    for item in self.__content:
+      if isinstance(item, Placeholder):
+        phs[pos] = item
+        pos += len(item.GetPresentation())
+      else:
+        pos += len(item)
+    presentation = self.GetPresentableContent()
+    for ph in self.GetPlaceholders():
+      for pos in FindOverlapping(presentation, ph.GetPresentation()):
+        # message contains the same text as a placeholder presentation
+        other_ph = phs.get(pos)
+        if ((not other_ph
+             and not IsSubstringInPlaceholder(pos, len(ph.GetPresentation()), phs))
+            or
+            (other_ph and len(other_ph.GetPresentation()) < len(ph.GetPresentation()))):
+          return  "message contains placeholder name '%s':\n%s" % (
+            ph.GetPresentation(), presentation)
+    return None
+
+
+  def __CopyTo(self, other):
+    """
+    Returns a copy of this BaseMessage.
+    """
+    assert isinstance(other,  self.__class__) or isinstance(self, other.__class__)
+    other.__source_encoding = self.__source_encoding
+    other.__content         = self.__content[:]
+    other.__description     = self.__description
+    other.__id              = self.__id
+    other.__num_instances   = self.__num_instances
+    other.__obsolete        = self.__obsolete
+    other.__name            = self.__name
+    other.__placeholders    = self.__placeholders[:]
+    other.__sequence_number = self.__sequence_number
+    other.__sources         = self.__sources[:]
+
+    return other
+
+  def HasText(self):
+    """Returns true iff this message has anything other than placeholders."""
+    for item in self.__content:
+      if not isinstance(item, Placeholder):
+        return True
+    return False
+
+# --------------------------------------------------------
+# The Message class represents original (English) messages
+
+class Message(BaseMessage):
+  # See BaseMessage constructor
+  def __init__(self, source_encoding, text=None, id=None,
+               description=None, meaning="", placeholders=None,
+               source=None, sequence_number=0, clone_from=None,
+               time_created=0, name=None, is_hidden = 0):
+
+    if clone_from is not None:
+      BaseMessage.__init__(self, None, clone_from=clone_from)
+      self.__meaning = clone_from.__meaning
+      self.__time_created = clone_from.__time_created
+      self.__is_hidden = clone_from.__is_hidden
+      return
+
+    BaseMessage.__init__(self, source_encoding, text, id, description,
+                         placeholders, source, sequence_number,
+                         name=name)
+    self.__meaning = meaning
+    self.__time_created = time_created
+    self.SetIsHidden(is_hidden)
+
+  # String representation
+  def __str__(self):
+    s = 'source: %s, id: %s, content: "%s", meaning: "%s", ' \
+        'description: "%s"' % \
+        (self.GetSourcesAsText(), self.GetId(), self.GetPresentableContent(),
+         self.__meaning, self.GetDescription())
+    if self.GetName() is not None:
+      s += ', name: "%s"' % self.GetName()
+    placeholders = self.GetPlaceholders()
+    for i in range(len(placeholders)):
+      s += ", placeholder[%d]: %s" % (i, placeholders[i])
+    return s
+
+  # Strips leading and trailing whitespace, and returns a tuple
+  # containing the leading and trailing space that was removed.
+  def Strip(self):
+    leading = trailing = ''
+    content = self.GetContent()
+    if len(content) > 0:
+      s0 = content[0]
+      if not isinstance(s0, Placeholder):
+        s = s0.lstrip()
+        leading = s0[:-len(s)]
+        content[0] = s
+
+      s0 = content[-1]
+      if not isinstance(s0, Placeholder):
+        s = s0.rstrip()
+        trailing = s0[len(s):]
+        content[-1] = s
+    return leading, trailing
+
+  # Generate an id by hashing message content
+  def GenerateId(self):
+    self.SetId(GenerateMessageId(self.GetPresentableContent(),
+                                 self.__meaning))
+    return self.GetId()
+
+  def GetMeaning(self):
+    return self.__meaning
+
+  def GetTimeCreated(self):
+    return self.__time_created
+
+  # Equality operator
+  def EqualTo(self, other, strict = 1):
+    # Check id, meaning, content
+    if self.GetId() != other.GetId():
+      return 0
+    if self.__meaning != other.__meaning:
+      return 0
+    if self.GetPresentableContent() != other.GetPresentableContent():
+      return 0
+    # Check descriptions if comparison is strict
+    if (strict and
+        self.GetDescription() is not None and
+        other.GetDescription() is not None and
+        self.GetDescription() != other.GetDescription()):
+      return 0
+    # Check placeholders
+    ph1 = self.GetPlaceholders()
+    ph2 = other.GetPlaceholders()
+    if len(ph1) != len(ph2):
+      return 0
+    for i in range(len(ph1)):
+      if not ph1[i].EqualTo(ph2[i], strict):
+        return 0
+
+    return 1
+
+  def Copy(self):
+    """
+    Returns a copy of this Message.
+    """
+    assert isinstance(self, Message)
+    return Message(None, clone_from=self)
+
+  def SetIsHidden(self, is_hidden):
+    """Sets whether this message should be hidden.
+
+    Args:
+      is_hidden : 0 or 1 - if the message should be hidden, 0 otherwise
+    """
+    if is_hidden not in [0, 1]:
+      raise MessageTranslationError("is_hidden must be 0 or 1, got %s")
+    self.__is_hidden = is_hidden
+
+  def IsHidden(self):
+    """Returns 1 if this message is hidden, and 0 otherwise."""
+    return self.__is_hidden
+
+# ----------------------------------------------------
+# The Translation class represents translated messages
+
+class Translation(BaseMessage):
+  # See BaseMessage constructor
+  def __init__(self, source_encoding, text=None, id=None,
+               description=None, placeholders=None, source=None,
+               sequence_number=0, clone_from=None, ignore_ph_errors=0,
+               name=None):
+    if clone_from is not None:
+      BaseMessage.__init__(self, None, clone_from=clone_from)
+      return
+
+    BaseMessage.__init__(self, source_encoding, text, id, description,
+                         placeholders, source, sequence_number,
+                         ignore_ph_errors=ignore_ph_errors, name=name)
+
+  # String representation
+  def __str__(self):
+    s = 'source: %s, id: %s, content: "%s", description: "%s"' % \
+        (self.GetSourcesAsText(), self.GetId(), self.GetPresentableContent(),
+         self.GetDescription());
+    placeholders = self.GetPlaceholders()
+    for i in range(len(placeholders)):
+      s += ", placeholder[%d]: %s" % (i, placeholders[i])
+    return s
+
+  # Equality operator
+  def EqualTo(self, other, strict=1):
+    # Check id and content
+    if self.GetId() != other.GetId():
+      return 0
+    if self.GetPresentableContent() != other.GetPresentableContent():
+      return 0
+    # Check placeholders
+    ph1 = self.GetPlaceholders()
+    ph2 = other.GetPlaceholders()
+    if len(ph1) != len(ph2):
+      return 0
+    for i in range(len(ph1)):
+      if not ph1[i].EqualTo(ph2[i], strict):
+        return 0
+
+    return 1
+
+  def Copy(self):
+    """
+    Returns a copy of this Translation.
+    """
+    return Translation(None, clone_from=self)
diff --git a/tools/grit/grit/format/__init__.py b/tools/grit/grit/format/__init__.py
new file mode 100644
index 0000000000..55d56b8cfd
--- /dev/null
+++ b/tools/grit/grit/format/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Module grit.format
+'''
+
+pass
diff --git a/tools/grit/grit/format/android_xml.py b/tools/grit/grit/format/android_xml.py
new file mode 100644
index 0000000000..7eb288891f
--- /dev/null
+++ b/tools/grit/grit/format/android_xml.py
@@ -0,0 +1,212 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Produces localized strings.xml files for Android.
+
+In cases where an "android" type output file is requested in a grd, the classes
+in android_xml will process the messages and translations to produce a valid
+strings.xml that is properly localized with the specified language.
+
+For example if the following output tag were to be included in a grd file
+  <outputs>
+    ...
+    <output filename="values-es/strings.xml" type="android" lang="es" />
+    ...
+  </outputs>
+
+for a grd file with the following messages:
+
+  <message name="IDS_HELLO" desc="Simple greeting">Hello</message>
+  <message name="IDS_WORLD" desc="The world">world</message>
+
+and there existed an appropriate xtb file containing the Spanish translations,
+then the output would be:
+
+  <?xml version="1.0" encoding="utf-8"?>
+  <resources xmlns:android="http://schemas.android.com/apk/res/android">
+    <string name="hello">"Hola"</string>
+    <string name="world">"mundo"</string>
+  </resources>
+
+which would be written to values-es/strings.xml and usable by the Android
+resource framework.
+
+Advanced usage
+--------------
+
+To process only certain messages in a grd file, tag each desired message by
+adding "android_java" to formatter_data. Then set the environmental variable
+ANDROID_JAVA_TAGGED_ONLY to "true" when building the grd file. For example:
+
+  <message name="IDS_HELLO" formatter_data="android_java">Hello</message>
+
+To generate Android plurals (aka "quantity strings"), use the ICU plural syntax
+in the grd file. This will automatically be transformed into a <purals> element
+in the output xml file. For example:
+
+  <message name="IDS_CATS">
+    {NUM_CATS, plural,
+    =1 {1 cat}
+    other {# cats}}
+  </message>
+
+  will produce
+
+  <plurals name="cats">
+    <item quantity="one">1 Katze</item>
+    <item quantity="other">%d Katzen</item>
+  </plurals>
+"""
+
+from __future__ import print_function
+
+import os
+import re
+import xml.sax.saxutils
+
+from grit import lazy_re
+from grit.node import message
+
+
+# When this environmental variable has value "true", only tagged messages will
+# be outputted.
+_TAGGED_ONLY_ENV_VAR = 'ANDROID_JAVA_TAGGED_ONLY'
+_TAGGED_ONLY_DEFAULT = False
+
+# In tagged-only mode, only messages with this tag will be ouputted.
+_EMIT_TAG = 'android_java'
+
+_NAME_PATTERN = lazy_re.compile(r'IDS_(?P<name>[A-Z0-9_]+)\Z')
+
+# Most strings are output as a <string> element. Note the double quotes
+# around the value to preserve whitespace.
+_STRING_TEMPLATE = u'<string name="%s">"%s"</string>\n'
+
+# Some strings are output as a <plurals> element.
+_PLURALS_TEMPLATE = '<plurals name="%s">\n%s</plurals>\n'
+_PLURALS_ITEM_TEMPLATE = '  <item quantity="%s">%s</item>\n'
+
+# Matches e.g. "{HELLO, plural, HOW ARE YOU DOING}", while capturing
+# "HOW ARE YOU DOING" in <items>.
+_PLURALS_PATTERN = lazy_re.compile(r'\{[A-Z_]+,\s*plural,(?P<items>.*)\}$',
+                                   flags=re.S)
+
+# Repeatedly matched against the <items> capture in _PLURALS_PATTERN,
+# to match "<quantity>{<value>}".
+_PLURALS_ITEM_PATTERN = lazy_re.compile(r'(?P<quantity>\S+?)\s*'
+                                        r'\{(?P<value>.*?)\}')
+_PLURALS_QUANTITY_MAP = {
+  '=0': 'zero',
+  'zero': 'zero',
+  '=1': 'one',
+  'one': 'one',
+  '=2': 'two',
+  'two': 'two',
+  'few': 'few',
+  'many': 'many',
+  'other': 'other',
+}
+
+
+def Format(root, lang='en', output_dir='.'):
+  yield ('<?xml version="1.0" encoding="utf-8"?>\n'
+          '<resources '
+          'xmlns:android="http://schemas.android.com/apk/res/android">\n')
+
+  tagged_only = _TAGGED_ONLY_DEFAULT
+  if _TAGGED_ONLY_ENV_VAR in os.environ:
+    tagged_only = os.environ[_TAGGED_ONLY_ENV_VAR].lower()
+    if tagged_only == 'true':
+      tagged_only = True
+    elif tagged_only == 'false':
+      tagged_only = False
+    else:
+      raise Exception('env variable ANDROID_JAVA_TAGGED_ONLY must have value '
+                      'true or false. Invalid value: %s' % tagged_only)
+
+  for item in root.ActiveDescendants():
+    with item:
+      if ShouldOutputNode(item, tagged_only):
+        yield _FormatMessage(item, lang)
+
+  yield '</resources>\n'
+
+
+def ShouldOutputNode(node, tagged_only):
+  """Returns true if node should be outputted.
+
+  Args:
+      node: a Node from the grd dom
+      tagged_only: true, if only tagged messages should be outputted
+  """
+  return (isinstance(node, message.MessageNode) and
+          (not tagged_only or _EMIT_TAG in node.formatter_data))
+
+
+def _FormatPluralMessage(message):
+  """Compiles ICU plural syntax to the body of an Android <plurals> element.
+
+  1. In a .grd file, we can write a plural string like this:
+
+    <message name="IDS_THINGS">
+      {NUM_THINGS, plural,
+      =1 {1 thing}
+      other {# things}}
+    </message>
+
+  2. The Android equivalent looks like this:
+
+    <plurals name="things">
+      <item quantity="one">1 thing</item>
+      <item quantity="other">%d things</item>
+    </plurals>
+
+  This method takes the body of (1) and converts it to the body of (2).
+
+  If the message is *not* a plural string, this function returns `None`.
+  If the message includes quantities without an equivalent format in Android,
+  it raises an exception.
+  """
+  ret = {}
+  plural_match = _PLURALS_PATTERN.match(message)
+  if not plural_match:
+    return None
+  body_in = plural_match.group('items').strip()
+  lines = []
+  quantities_so_far = set()
+  for item_match in _PLURALS_ITEM_PATTERN.finditer(body_in):
+    quantity_in = item_match.group('quantity')
+    quantity_out = _PLURALS_QUANTITY_MAP.get(quantity_in)
+    value_in = item_match.group('value')
+    value_out = '"' + value_in.replace('#', '%d') + '"'
+    if quantity_out:
+      # only one line per quantity out (https://crbug.com/787488)
+      if quantity_out not in quantities_so_far:
+        quantities_so_far.add(quantity_out)
+        lines.append(_PLURALS_ITEM_TEMPLATE % (quantity_out, value_out))
+    else:
+      raise Exception('Unsupported plural quantity for android '
+                      'strings.xml: %s' % quantity_in)
+  return ''.join(lines)
+
+
+def _FormatMessage(item, lang):
+  """Writes out a single string as a <resource/> element."""
+
+  mangled_name = item.GetTextualIds()[0]
+  match = _NAME_PATTERN.match(mangled_name)
+  if not match:
+    raise Exception('Unexpected resource name: %s' % mangled_name)
+  name = match.group('name').lower()
+
+  value = item.ws_at_start + item.Translate(lang) + item.ws_at_end
+  # Replace < > & with < > & to ensure we generate valid XML and
+  # replace ' " with \' \" to conform to Android's string formatting rules.
+  value = xml.sax.saxutils.escape(value, {"'": "\\'", '"': '\\"'})
+
+  plurals = _FormatPluralMessage(value)
+  if plurals:
+    return _PLURALS_TEMPLATE % (name, plurals)
+  else:
+    return _STRING_TEMPLATE % (name, value)
diff --git a/tools/grit/grit/format/android_xml_unittest.py b/tools/grit/grit/format/android_xml_unittest.py
new file mode 100644
index 0000000000..d9f476fddf
--- /dev/null
+++ b/tools/grit/grit/format/android_xml_unittest.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for android_xml.py."""
+
+from __future__ import print_function
+
+import os
+import sys
+import unittest
+
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from six import StringIO
+
+from grit import util
+from grit.format import android_xml
+from grit.node import message
+from grit.tool import build
+
+
+class AndroidXmlUnittest(unittest.TestCase):
+
+  def testMessages(self):
+    root = util.ParseGrdForUnittest(r"""
+        <messages>
+          <message name="IDS_SIMPLE" desc="A vanilla string">
+            Martha
+          </message>
+          <message name="IDS_ONE_LINE" desc="On one line">sat and wondered</message>
+          <message name="IDS_QUOTES" desc="A string with quotation marks">
+            out loud, "Why don't I build a flying car?"
+          </message>
+          <message name="IDS_MULTILINE" desc="A string split over several lines">
+            She gathered
+wood, charcoal, and
+a sledge hammer.
+          </message>
+          <message name="IDS_WHITESPACE" desc="A string with extra whitespace.">
+            '''   How old fashioned  --  she thought. '''
+          </message>
+          <message name="IDS_PLACEHOLDERS" desc="A string with placeholders">
+            I'll buy a <ph name="WAVELENGTH">%d<ex>200</ex></ph> nm laser at <ph name="STORE_NAME">%s<ex>the grocery store</ex></ph>.
+          </message>
+          <message name="IDS_PLURALS" desc="A string using the ICU plural format">
+            {NUM_THINGS, plural,
+            =1 {Maybe I'll get one laser.}
+            other {Maybe I'll get # lasers.}}
+          </message>
+          <message name="IDS_PLURALS_NO_SPACE" desc="A string using the ICU plural format with no space">
+            {NUM_MISSISSIPPIS, plural,
+            =1{OneMississippi}other{ManyMississippis}}
+          </message>
+        </messages>
+        """)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('android', 'en'), buf)
+    output = buf.getvalue()
+    expected = r"""
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<string name="simple">"Martha"</string>
+<string name="one_line">"sat and wondered"</string>
+<string name="quotes">"out loud, \"Why don\'t I build a flying car?\""</string>
+<string name="multiline">"She gathered
+wood, charcoal, and
+a sledge hammer."</string>
+<string name="whitespace">"   How old fashioned  --  she thought. "</string>
+<string name="placeholders">"I\'ll buy a %d nm laser at %s."</string>
+<plurals name="plurals">
+  <item quantity="one">"Maybe I\'ll get one laser."</item>
+  <item quantity="other">"Maybe I\'ll get %d lasers."</item>
+</plurals>
+<plurals name="plurals_no_space">
+  <item quantity="one">"OneMississippi"</item>
+  <item quantity="other">"ManyMississippis"</item>
+</plurals>
+</resources>
+"""
+    self.assertEqual(output.strip(), expected.strip())
+
+
+  def testConflictingPlurals(self):
+    root = util.ParseGrdForUnittest(r"""
+        <messages>
+          <message name="IDS_PLURALS" desc="A string using the ICU plural format">
+            {NUM_THINGS, plural,
+            =1 {Maybe I'll get one laser.}
+            one {Maybe I'll get one laser.}
+            other {Maybe I'll get # lasers.}}
+          </message>
+        </messages>
+        """)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('android', 'en'), buf)
+    output = buf.getvalue()
+    expected = r"""
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+<plurals name="plurals">
+  <item quantity="one">"Maybe I\'ll get one laser."</item>
+  <item quantity="other">"Maybe I\'ll get %d lasers."</item>
+</plurals>
+</resources>
+"""
+    self.assertEqual(output.strip(), expected.strip())
+
+
+  def testTaggedOnly(self):
+    root = util.ParseGrdForUnittest(r"""
+        <messages>
+          <message name="IDS_HELLO" desc="" formatter_data="android_java">
+            Hello
+          </message>
+          <message name="IDS_WORLD" desc="">
+            world
+          </message>
+        </messages>
+        """)
+
+    msg_hello, msg_world = root.GetChildrenOfType(message.MessageNode)
+    self.assertTrue(android_xml.ShouldOutputNode(msg_hello, tagged_only=True))
+    self.assertFalse(android_xml.ShouldOutputNode(msg_world, tagged_only=True))
+    self.assertTrue(android_xml.ShouldOutputNode(msg_hello, tagged_only=False))
+    self.assertTrue(android_xml.ShouldOutputNode(msg_world, tagged_only=False))
+
+
+class DummyOutput(object):
+
+  def __init__(self, type, language):
+    self.type = type
+    self.language = language
+
+  def GetType(self):
+    return self.type
+
+  def GetLanguage(self):
+    return self.language
+
+  def GetOutputFilename(self):
+    return 'hello.gif'
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/c_format.py b/tools/grit/grit/format/c_format.py
new file mode 100644
index 0000000000..16809a9f70
--- /dev/null
+++ b/tools/grit/grit/format/c_format.py
@@ -0,0 +1,95 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Formats as a .C file for compilation.
+"""
+
+from __future__ import print_function
+
+import codecs
+import os
+import re
+
+import six
+
+from grit import util
+
+
+def _FormatHeader(root, output_dir):
+  """Returns the required preamble for C files."""
+  # Find the location of the resource header file, so that we can include
+  # it.
+  resource_header = 'resource.h'  # fall back to this
+  for output in root.GetOutputFiles():
+    if output.attrs['type'] == 'rc_header':
+      resource_header = os.path.abspath(output.GetOutputFilename())
+      resource_header = util.MakeRelativePath(output_dir, resource_header)
+  return """// This file is automatically generated by GRIT.  Do not edit.
+
+#include "%s"
+
+// All strings are UTF-8
+""" % (resource_header)
+# end _FormatHeader() function
+
+
+def Format(root, lang='en', output_dir='.'):
+  """Outputs a C switch statement representing the string table."""
+  from grit.node import message
+  assert isinstance(lang, six.string_types)
+
+  yield _FormatHeader(root, output_dir)
+
+  yield 'const char* GetString(int id) {\n  switch (id) {'
+
+  for item in root.ActiveDescendants():
+    with item:
+      if isinstance(item, message.MessageNode):
+        yield _FormatMessage(item, lang)
+
+  yield '\n    default:\n      return 0;\n  }\n}\n'
+
+
+def _HexToOct(match):
+  "Return the octal form of the hex numbers"
+  hex = match.group("hex")
+  result = ""
+  while len(hex):
+    next_num = int(hex[2:4], 16)
+    result += "\\" + '%03o' % next_num
+    hex = hex[4:]
+  return match.group("escaped_backslashes") + result
+
+
+def _FormatMessage(item, lang):
+  """Format a single <message> element."""
+
+  message = item.ws_at_start + item.Translate(lang) + item.ws_at_end
+  # Output message with non-ascii chars escaped as octal numbers C's grammar
+  # allows escaped hexadecimal numbers to be infinite, but octal is always of
+  # the form \OOO.  Python 3 doesn't support string-escape, so we have to jump
+  # through some hoops here via codecs.escape_encode.
+  # This basically does:
+  #   - message - the starting string
+  #   - message.encode(...) - convert to bytes
+  #   - codecs.escape_encode(...) - convert non-ASCII bytes to \x## escapes
+  #   - (...).decode() - convert bytes back to a string
+  message = codecs.escape_encode(message.encode('utf-8'))[0].decode('utf-8')
+  # an escaped char is (\xHH)+ but only if the initial
+  # backslash is not escaped.
+  not_a_backslash = r"(^|[^\\])"  # beginning of line or a non-backslash char
+  escaped_backslashes = not_a_backslash + r"(\\\\)*"
+  hex_digits = r"((\\x)[0-9a-f]{2})+"
+  two_digit_hex_num = re.compile(
+    r"(?P<escaped_backslashes>%s)(?P<hex>%s)"
+    % (escaped_backslashes, hex_digits))
+  message = two_digit_hex_num.sub(_HexToOct, message)
+  # unescape \ (convert \\ back to \)
+  message = message.replace('\\\\', '\\')
+  message = message.replace('"', '\\"')
+  message = util.LINEBREAKS.sub(r'\\n', message)
+
+  name_attr = item.GetTextualIds()[0]
+
+  return '\n    case %s:\n      return "%s";' % (name_attr, message)
diff --git a/tools/grit/grit/format/c_format_unittest.py b/tools/grit/grit/format/c_format_unittest.py
new file mode 100644
index 0000000000..380120c42f
--- /dev/null
+++ b/tools/grit/grit/format/c_format_unittest.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for c_format.py.
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import util
+from grit.tool import build
+
+
+class CFormatUnittest(unittest.TestCase):
+
+  def testMessages(self):
+    root = util.ParseGrdForUnittest(u"""
+    <messages>
+      <message name="IDS_QUESTIONS">Do you want to play questions?</message>
+      <message name="IDS_QUOTES">
+      "What's in a name, <ph name="NAME">%s<ex>Brandon</ex></ph>?"
+      </message>
+      <message name="IDS_LINE_BREAKS">
+          Was that rhetoric?
+No.
+Statement.  Two all.  Game point.
+</message>
+      <message name="IDS_NON_ASCII">
+         \u00f5\\xc2\\xa4\\\u00a4\\\\xc3\\xb5\u4924
+      </message>
+    </messages>
+      """)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('c_format', 'en'), buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+    self.assertEqual(u"""\
+#include "resource.h"
+const char* GetString(int id) {
+  switch (id) {
+    case IDS_QUESTIONS:
+      return "Do you want to play questions?";
+    case IDS_QUOTES:
+      return "\\"What\\'s in a name, %s?\\"";
+    case IDS_LINE_BREAKS:
+      return "Was that rhetoric?\\nNo.\\nStatement.  Two all.  Game point.";
+    case IDS_NON_ASCII:
+      return "\\303\\265\\xc2\\xa4\\\\302\\244\\\\xc3\\xb5\\344\\244\\244";
+    default:
+      return 0;
+  }
+}""", output)
+
+
+class DummyOutput(object):
+
+  def __init__(self, type, language):
+    self.type = type
+    self.language = language
+
+  def GetType(self):
+    return self.type
+
+  def GetLanguage(self):
+    return self.language
+
+  def GetOutputFilename(self):
+    return 'hello.gif'
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/chrome_messages_json.py b/tools/grit/grit/format/chrome_messages_json.py
new file mode 100644
index 0000000000..88ec1d914b
--- /dev/null
+++ b/tools/grit/grit/format/chrome_messages_json.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Formats as a .json file that can be used to localize Google Chrome
+extensions."""
+
+from __future__ import print_function
+
+from json import JSONEncoder
+
+from grit import constants
+from grit.node import message
+
+def Format(root, lang='en', output_dir='.'):
+  """Format the messages as JSON."""
+  yield '{'
+
+  encoder = JSONEncoder(ensure_ascii=False)
+  format = '"%s":{"message":%s%s}'
+  placeholder_format = '"%i":{"content":"$%i"}'
+  first = True
+  for child in root.ActiveDescendants():
+    if isinstance(child, message.MessageNode):
+      id = child.attrs['name']
+      if id.startswith('IDR_') or id.startswith('IDS_'):
+        id = id[4:]
+
+      translation_missing = child.GetCliques()[0].clique.get(lang) is None;
+      if (child.ShouldFallbackToEnglish() and translation_missing and
+          lang != constants.FAKE_BIDI):
+          # Skip the string if it's not translated. Chrome will fallback
+          # to English automatically.
+          continue
+
+      loc_message = encoder.encode(child.ws_at_start + child.Translate(lang) +
+                                   child.ws_at_end)
+
+      # Replace $n place-holders with $n$ and add an appropriate "placeholders"
+      # entry. Note that chrome.i18n.getMessage only supports 9 placeholders:
+      # https://developer.chrome.com/extensions/i18n#method-getMessage
+      placeholders = ''
+      for i in range(1, 10):
+        if loc_message.find('$%d' % i) == -1:
+          break
+        loc_message = loc_message.replace('$%d' % i, '$%d$' % i)
+        if placeholders:
+          placeholders += ','
+        placeholders += placeholder_format % (i, i)
+
+      if not first:
+        yield ','
+      first = False
+
+      if placeholders:
+        placeholders = ',"placeholders":{%s}' % placeholders
+      yield format % (id, loc_message, placeholders)
+
+  yield '}'
diff --git a/tools/grit/grit/format/chrome_messages_json_unittest.py b/tools/grit/grit/format/chrome_messages_json_unittest.py
new file mode 100644
index 0000000000..a54e6bdc1c
--- /dev/null
+++ b/tools/grit/grit/format/chrome_messages_json_unittest.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for chrome_messages_json.py.
+"""
+
+from __future__ import print_function
+
+import json
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit import util
+from grit.tool import build
+
+class ChromeMessagesJsonFormatUnittest(unittest.TestCase):
+
+  # The default unittest diff limit is too low for our unittests.
+  # Allow the framework to show the full diff output all the time.
+  maxDiff = None
+
+  def testMessages(self):
+    root = util.ParseGrdForUnittest(u"""
+    <messages>
+      <message name="IDS_SIMPLE_MESSAGE">
+              Simple message.
+      </message>
+      <message name="IDS_QUOTES">
+              element\u2019s \u201c<ph name="NAME">%s<ex>name</ex></ph>\u201d attribute
+      </message>
+      <message name="IDS_PLACEHOLDERS">
+              <ph name="ERROR_COUNT">%1$d<ex>1</ex></ph> error, <ph name="WARNING_COUNT">%2$d<ex>1</ex></ph> warning
+      </message>
+      <message name="IDS_PLACEHOLDERS_SUBSTITUTED_BY_GETMESSAGE">
+              <ph name="BEGIN">$1<ex>a</ex></ph>test<ph name="END">$2<ex>b</ex></ph>
+      </message>
+      <message name="IDS_STARTS_WITH_SPACE">
+              ''' (<ph name="COUNT">%d<ex>2</ex></ph>)
+      </message>
+      <message name="IDS_ENDS_WITH_SPACE">
+              (<ph name="COUNT">%d<ex>2</ex></ph>) '''
+      </message>
+      <message name="IDS_SPACE_AT_BOTH_ENDS">
+              ''' (<ph name="COUNT">%d<ex>2</ex></ph>) '''
+      </message>
+      <message name="IDS_DOUBLE_QUOTES">
+              A "double quoted" message.
+      </message>
+      <message name="IDS_BACKSLASH">
+              \\
+      </message>
+    </messages>
+    """)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'en'),
+                                buf)
+    output = buf.getvalue()
+    test = u"""
+{
+  "SIMPLE_MESSAGE": {
+    "message": "Simple message."
+  },
+  "QUOTES": {
+    "message": "element\u2019s \u201c%s\u201d attribute"
+  },
+  "PLACEHOLDERS": {
+    "message": "%1$d error, %2$d warning"
+  },
+  "PLACEHOLDERS_SUBSTITUTED_BY_GETMESSAGE": {
+    "message": "$1$test$2$",
+    "placeholders": {
+      "1": {
+        "content": "$1"
+      },
+      "2": {
+        "content": "$2"
+      }
+    }
+  },
+  "STARTS_WITH_SPACE": {
+    "message": " (%d)"
+  },
+  "ENDS_WITH_SPACE": {
+    "message": "(%d) "
+  },
+  "SPACE_AT_BOTH_ENDS": {
+    "message": " (%d) "
+  },
+  "DOUBLE_QUOTES": {
+    "message": "A \\"double quoted\\" message."
+  },
+  "BACKSLASH": {
+    "message": "\\\\"
+  }
+}
+"""
+    self.assertEqual(json.loads(test), json.loads(output))
+
+  def testTranslations(self):
+    root = util.ParseGrdForUnittest("""
+    <messages>
+        <message name="ID_HELLO">Hello!</message>
+        <message name="ID_HELLO_USER">Hello <ph name="USERNAME">%s<ex>
+          Joi</ex></ph></message>
+      </messages>
+    """)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'fr'),
+                                buf)
+    output = buf.getvalue()
+    test = u"""
+{
+  "ID_HELLO": {
+    "message": "H\u00e9P\u00e9ll\u00f4P\u00f4!"
+  },
+  "ID_HELLO_USER": {
+    "message": "H\u00e9P\u00e9ll\u00f4P\u00f4 %s"
+  }
+}
+"""
+    self.assertEqual(json.loads(test), json.loads(output))
+
+  def testSkipMissingTranslations(self):
+    grd = """<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" current_release="3" source_lang_id="en"
+    base_dir="%s">
+  <outputs>
+  </outputs>
+  <release seq="3" allow_pseudo="False">
+    <messages fallback_to_english="true">
+      <message name="ID_HELLO_NO_TRANSLATION">Hello not translated</message>
+    </messages>
+  </release>
+</grit>"""
+    root = grd_reader.Parse(StringIO(grd), dir=".")
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'fr'),
+                                buf)
+    output = buf.getvalue()
+    test = u'{}'
+    self.assertEqual(test, output)
+
+  def testVerifyMinification(self):
+    root = util.ParseGrdForUnittest(u"""
+    <messages>
+      <message name="IDS">
+        <ph name="BEGIN">$1<ex>a</ex></ph>test<ph name="END">$2<ex>b</ex></ph>
+      </message>
+    </messages>
+    """)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('chrome_messages_json', 'en'),
+                                buf)
+    output = buf.getvalue()
+    test = (u'{"IDS":{"message":"$1$test$2$","placeholders":'
+            u'{"1":{"content":"$1"},"2":{"content":"$2"}}}}')
+    self.assertEqual(test, output)
+
+
+class DummyOutput(object):
+
+  def __init__(self, type, language):
+    self.type = type
+    self.language = language
+
+  def GetType(self):
+    return self.type
+
+  def GetLanguage(self):
+    return self.language
+
+  def GetOutputFilename(self):
+    return 'hello.gif'
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/data_pack.py b/tools/grit/grit/format/data_pack.py
new file mode 100644
index 0000000000..f7128a4725
--- /dev/null
+++ b/tools/grit/grit/format/data_pack.py
@@ -0,0 +1,321 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Support for formatting a data pack file used for platform agnostic resource
+files.
+"""
+
+from __future__ import print_function
+
+import collections
+import os
+import struct
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import six
+
+from grit import util
+from grit.node import include
+from grit.node import message
+from grit.node import structure
+
+
+PACK_FILE_VERSION = 5
+BINARY, UTF8, UTF16 = range(3)
+
+
+GrdInfoItem = collections.namedtuple('GrdInfoItem',
+                                     ['textual_id', 'id', 'path'])
+
+
+class WrongFileVersion(Exception):
+  pass
+
+
+class CorruptDataPack(Exception):
+  pass
+
+
+class DataPackSizes(object):
+  def __init__(self, header, id_table, alias_table, data):
+    self.header = header
+    self.id_table = id_table
+    self.alias_table = alias_table
+    self.data = data
+
+  @property
+  def total(self):
+    return sum(v for v in self.__dict__.values())
+
+  def __iter__(self):
+    yield ('header', self.header)
+    yield ('id_table', self.id_table)
+    yield ('alias_table', self.alias_table)
+    yield ('data', self.data)
+
+  def __eq__(self, other):
+    return self.__dict__ == other.__dict__
+
+  def __repr__(self):
+    return self.__class__.__name__ + repr(self.__dict__)
+
+
+class DataPackContents(object):
+  def __init__(self, resources, encoding, version, aliases, sizes):
+    # Map of resource_id -> str.
+    self.resources = resources
+    # Encoding (int).
+    self.encoding = encoding
+    # Version (int).
+    self.version = version
+    # Map of resource_id->canonical_resource_id
+    self.aliases = aliases
+    # DataPackSizes instance.
+    self.sizes = sizes
+
+
+def Format(root, lang='en', output_dir='.'):
+  """Writes out the data pack file format (platform agnostic resource file)."""
+  id_map = root.GetIdMap()
+  data = {}
+  root.info = []
+  for node in root.ActiveDescendants():
+    with node:
+      if isinstance(node, (include.IncludeNode, message.MessageNode,
+                           structure.StructureNode)):
+        value = node.GetDataPackValue(lang, util.BINARY)
+        if value is not None:
+          resource_id = id_map[node.GetTextualIds()[0]]
+          data[resource_id] = value
+          root.info.append('{},{},{}'.format(
+              node.attrs.get('name'), resource_id, node.source))
+  return WriteDataPackToString(data, UTF8)
+
+
+def ReadDataPack(input_file):
+  return ReadDataPackFromString(util.ReadFile(input_file, util.BINARY))
+
+
+def ReadDataPackFromString(data):
+  """Reads a data pack file and returns a dictionary."""
+  # Read the header.
+  version = struct.unpack('<I', data[:4])[0]
+  if version == 4:
+    resource_count, encoding = struct.unpack('<IB', data[4:9])
+    alias_count = 0
+    header_size = 9
+  elif version == 5:
+    encoding, resource_count, alias_count = struct.unpack('<BxxxHH', data[4:12])
+    header_size = 12
+  else:
+    raise WrongFileVersion('Found version: ' + str(version))
+
+  resources = {}
+  kIndexEntrySize = 2 + 4  # Each entry is a uint16 and a uint32.
+  def entry_at_index(idx):
+    offset = header_size + idx * kIndexEntrySize
+    return struct.unpack('<HI', data[offset:offset + kIndexEntrySize])
+
+  prev_resource_id, prev_offset = entry_at_index(0)
+  for i in range(1, resource_count + 1):
+    resource_id, offset = entry_at_index(i)
+    resources[prev_resource_id] = data[prev_offset:offset]
+    prev_resource_id, prev_offset = resource_id, offset
+
+  id_table_size = (resource_count + 1) * kIndexEntrySize
+  # Read the alias table.
+  kAliasEntrySize = 2 + 2  # uint16, uint16
+  def alias_at_index(idx):
+    offset = header_size + id_table_size + idx * kAliasEntrySize
+    return struct.unpack('<HH', data[offset:offset + kAliasEntrySize])
+
+  aliases = {}
+  for i in range(alias_count):
+    resource_id, index = alias_at_index(i)
+    aliased_id = entry_at_index(index)[0]
+    aliases[resource_id] = aliased_id
+    resources[resource_id] = resources[aliased_id]
+
+  alias_table_size = kAliasEntrySize * alias_count
+  sizes = DataPackSizes(
+      header_size, id_table_size, alias_table_size,
+      len(data) - header_size - id_table_size - alias_table_size)
+  assert sizes.total == len(data), 'original={} computed={}'.format(
+      len(data), sizes.total)
+  return DataPackContents(resources, encoding, version, aliases, sizes)
+
+
+def WriteDataPackToString(resources, encoding):
+  """Returns bytes with a map of id=>data in the data pack format."""
+  ret = []
+
+  # Compute alias map.
+  resource_ids = sorted(resources)
+  # Use reversed() so that for duplicates lower IDs clobber higher ones.
+  id_by_data = {resources[k]: k for k in reversed(resource_ids)}
+  # Map of resource_id -> resource_id, where value < key.
+  alias_map = {k: id_by_data[v] for k, v in resources.items()
+               if id_by_data[v] != k}
+
+  # Write file header.
+  resource_count = len(resources) - len(alias_map)
+  # Padding bytes added for alignment.
+  ret.append(struct.pack('<IBxxxHH', PACK_FILE_VERSION, encoding,
+                         resource_count, len(alias_map)))
+  HEADER_LENGTH = 4 + 4 + 2 + 2
+
+  # Each main table entry is: uint16 + uint32 (and an extra entry at the end).
+  # Each alias table entry is: uint16 + uint16.
+  data_offset = HEADER_LENGTH + (resource_count + 1) * 6 + len(alias_map) * 4
+
+  # Write main table.
+  index_by_id = {}
+  deduped_data = []
+  index = 0
+  for resource_id in resource_ids:
+    if resource_id in alias_map:
+      continue
+    data = resources[resource_id]
+    if isinstance(data, six.text_type):
+      data = data.encode('utf-8')
+    index_by_id[resource_id] = index
+    ret.append(struct.pack('<HI', resource_id, data_offset))
+    data_offset += len(data)
+    deduped_data.append(data)
+    index += 1
+
+  assert index == resource_count
+  # Add an extra entry at the end.
+  ret.append(struct.pack('<HI', 0, data_offset))
+
+  # Write alias table.
+  for resource_id in sorted(alias_map):
+    index = index_by_id[alias_map[resource_id]]
+    ret.append(struct.pack('<HH', resource_id, index))
+
+  # Write data.
+  ret.extend(deduped_data)
+  return b''.join(ret)
+
+
+def WriteDataPack(resources, output_file, encoding):
+  """Writes a map of id=>data into output_file as a data pack."""
+  content = WriteDataPackToString(resources, encoding)
+  with open(output_file, 'wb') as file:
+    file.write(content)
+
+
+def ReadGrdInfo(grd_file):
+  info_dict = {}
+  with open(grd_file + '.info', 'rt') as f:
+    for line in f:
+      item = GrdInfoItem._make(line.strip().split(','))
+      info_dict[int(item.id)] = item
+  return info_dict
+
+
+def RePack(output_file, input_files, whitelist_file=None,
+           suppress_removed_key_output=False,
+           output_info_filepath=None):
+  """Write a new data pack file by combining input pack files.
+
+  Args:
+      output_file: path to the new data pack file.
+      input_files: a list of paths to the data pack files to combine.
+      whitelist_file: path to the file that contains the list of resource IDs
+                      that should be kept in the output file or None to include
+                      all resources.
+      suppress_removed_key_output: allows the caller to suppress the output from
+                                   RePackFromDataPackStrings.
+      output_info_file: If not None, specify the output .info filepath.
+
+  Raises:
+      KeyError: if there are duplicate keys or resource encoding is
+      inconsistent.
+  """
+  input_data_packs = [ReadDataPack(filename) for filename in input_files]
+  input_info_files = [filename + '.info' for filename in input_files]
+  whitelist = None
+  if whitelist_file:
+    lines = util.ReadFile(whitelist_file, 'utf-8').strip().splitlines()
+    if not lines:
+      raise Exception('Whitelist file should not be empty')
+    whitelist = set(int(x) for x in lines)
+  inputs = [(p.resources, p.encoding) for p in input_data_packs]
+  resources, encoding = RePackFromDataPackStrings(
+      inputs, whitelist, suppress_removed_key_output)
+  WriteDataPack(resources, output_file, encoding)
+  if output_info_filepath is None:
+    output_info_filepath = output_file + '.info'
+  with open(output_info_filepath, 'w') as output_info_file:
+    for filename in input_info_files:
+      with open(filename, 'r') as info_file:
+        output_info_file.writelines(info_file.readlines())
+
+
+def RePackFromDataPackStrings(inputs, whitelist,
+                              suppress_removed_key_output=False):
+  """Combines all inputs into one.
+
+  Args:
+      inputs: a list of (resources_by_id, encoding) tuples to be combined.
+      whitelist: a list of resource IDs that should be kept in the output string
+                 or None to include all resources.
+      suppress_removed_key_output: Do not print removed keys.
+
+  Returns:
+      Returns (resources_by_id, encoding).
+
+  Raises:
+      KeyError: if there are duplicate keys or resource encoding is
+      inconsistent.
+  """
+  resources = {}
+  encoding = None
+  for input_resources, input_encoding in inputs:
+    # Make sure we have no dups.
+    duplicate_keys = set(input_resources.keys()) & set(resources.keys())
+    if duplicate_keys:
+      raise KeyError('Duplicate keys: ' + str(list(duplicate_keys)))
+
+    # Make sure encoding is consistent.
+    if encoding in (None, BINARY):
+      encoding = input_encoding
+    elif input_encoding not in (BINARY, encoding):
+      raise KeyError('Inconsistent encodings: ' + str(encoding) +
+                     ' vs ' + str(input_encoding))
+
+    if whitelist:
+      whitelisted_resources = dict([(key, input_resources[key])
+                                    for key in input_resources.keys()
+                                    if key in whitelist])
+      resources.update(whitelisted_resources)
+      removed_keys = [key for key in input_resources.keys()
+                      if key not in whitelist]
+      if not suppress_removed_key_output:
+        for key in removed_keys:
+          print('RePackFromDataPackStrings Removed Key:', key)
+    else:
+      resources.update(input_resources)
+
+  # Encoding is 0 for BINARY, 1 for UTF8 and 2 for UTF16
+  if encoding is None:
+    encoding = BINARY
+  return resources, encoding
+
+
+def main():
+  # Write a simple file.
+  data = {1: '', 4: 'this is id 4', 6: 'this is id 6', 10: ''}
+  WriteDataPack(data, 'datapack1.pak', UTF8)
+  data2 = {1000: 'test', 5: 'five'}
+  WriteDataPack(data2, 'datapack2.pak', UTF8)
+  print('wrote datapack1 and datapack2 to current directory.')
+
+
+if __name__ == '__main__':
+  main()
diff --git a/tools/grit/grit/format/data_pack_unittest.py b/tools/grit/grit/format/data_pack_unittest.py
new file mode 100644
index 0000000000..fcd7035473
--- /dev/null
+++ b/tools/grit/grit/format/data_pack_unittest.py
@@ -0,0 +1,102 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.data_pack'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit.format import data_pack
+
+
+class FormatDataPackUnittest(unittest.TestCase):
+  def testReadDataPackV4(self):
+    expected_data = (
+        b'\x04\x00\x00\x00'                  # header(version
+        b'\x04\x00\x00\x00'                  #        no. entries,
+        b'\x01'                              #        encoding)
+        b'\x01\x00\x27\x00\x00\x00'          # index entry 1
+        b'\x04\x00\x27\x00\x00\x00'          # index entry 4
+        b'\x06\x00\x33\x00\x00\x00'          # index entry 6
+        b'\x0a\x00\x3f\x00\x00\x00'          # index entry 10
+        b'\x00\x00\x3f\x00\x00\x00'          # extra entry for the size of last
+        b'this is id 4this is id 6')         # data
+    expected_data_pack = data_pack.DataPackContents(
+        {
+            1: b'',
+            4: b'this is id 4',
+            6: b'this is id 6',
+            10: b'',
+        }, data_pack.UTF8, 4, {}, data_pack.DataPackSizes(9, 30, 0, 24))
+    loaded = data_pack.ReadDataPackFromString(expected_data)
+    self.assertDictEqual(expected_data_pack.__dict__, loaded.__dict__)
+
+  def testReadWriteDataPackV5(self):
+    expected_data = (
+        b'\x05\x00\x00\x00'                  # version
+        b'\x01\x00\x00\x00'                  # encoding & padding
+        b'\x03\x00'                          # resource_count
+        b'\x01\x00'                          # alias_count
+        b'\x01\x00\x28\x00\x00\x00'          # index entry 1
+        b'\x04\x00\x28\x00\x00\x00'          # index entry 4
+        b'\x06\x00\x34\x00\x00\x00'          # index entry 6
+        b'\x00\x00\x40\x00\x00\x00'          # extra entry for the size of last
+        b'\x0a\x00\x01\x00'                  # alias table
+        b'this is id 4this is id 6')         # data
+    input_resources = {
+        1: b'',
+        4: b'this is id 4',
+        6: b'this is id 6',
+        10: b'this is id 4',
+    }
+    data = data_pack.WriteDataPackToString(input_resources, data_pack.UTF8)
+    self.assertEquals(data, expected_data)
+
+    expected_data_pack = data_pack.DataPackContents({
+        1: b'',
+        4: input_resources[4],
+        6: input_resources[6],
+        10: input_resources[4],
+    }, data_pack.UTF8, 5, {10: 4}, data_pack.DataPackSizes(12, 24, 4, 24))
+    loaded = data_pack.ReadDataPackFromString(expected_data)
+    self.assertDictEqual(expected_data_pack.__dict__, loaded.__dict__)
+
+  def testRePackUnittest(self):
+    expected_with_whitelist = {
+        1: 'Never gonna', 10: 'give you up', 20: 'Never gonna let',
+        30: 'you down', 40: 'Never', 50: 'gonna run around and',
+        60: 'desert you'}
+    expected_without_whitelist = {
+        1: 'Never gonna', 10: 'give you up', 20: 'Never gonna let', 65: 'Close',
+        30: 'you down', 40: 'Never', 50: 'gonna run around and', 4: 'click',
+        60: 'desert you', 6: 'chirr', 32: 'oops, try again', 70: 'Awww, snap!'}
+    inputs = [{1: 'Never gonna', 4: 'click', 6: 'chirr', 10: 'give you up'},
+              {20: 'Never gonna let', 30: 'you down', 32: 'oops, try again'},
+              {40: 'Never', 50: 'gonna run around and', 60: 'desert you'},
+              {65: 'Close', 70: 'Awww, snap!'}]
+    whitelist = [1, 10, 20, 30, 40, 50, 60]
+    inputs = [(i, data_pack.UTF8) for i in inputs]
+
+    # RePack using whitelist
+    output, _ = data_pack.RePackFromDataPackStrings(
+        inputs, whitelist, suppress_removed_key_output=True)
+    self.assertDictEqual(expected_with_whitelist, output,
+                         'Incorrect resource output')
+
+    # RePack a None whitelist
+    output, _ = data_pack.RePackFromDataPackStrings(
+        inputs, None, suppress_removed_key_output=True)
+    self.assertDictEqual(expected_without_whitelist, output,
+                         'Incorrect resource output')
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/gen_predetermined_ids.py b/tools/grit/grit/format/gen_predetermined_ids.py
new file mode 100644
index 0000000000..9b2aa7b1a5
--- /dev/null
+++ b/tools/grit/grit/format/gen_predetermined_ids.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""
+A tool to generate a predetermined resource ids file that can be used as an
+input to grit via the -p option. This is meant to be run manually every once in
+a while and its output checked in. See tools/gritsettings/README.md for details.
+"""
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+
+# Regular expression for parsing the #define macro format. Matches both the
+# version of the macro with whitelist support and the one without. For example,
+# Without generate whitelist flag:
+#   #define IDS_FOO_MESSAGE 1234
+# With generate whitelist flag:
+#   #define IDS_FOO_MESSAGE (::ui::WhitelistedResource<1234>(), 1234)
+RESOURCE_EXTRACT_REGEX = re.compile(r'^#define (\S*).* (\d+)\)?$', re.MULTILINE)
+
+ORDERED_RESOURCE_IDS_REGEX = re.compile(r'^Resource=(\d*)$', re.MULTILINE)
+
+
+def _GetResourceNameIdPairsIter(string_to_scan):
+  """Gets an iterator of the resource name and id pairs of the given string.
+
+  Scans the input string for lines of the form "#define NAME ID" and returns
+  an iterator over all matching (NAME, ID) pairs.
+
+  Args:
+    string_to_scan: The input string to scan.
+
+  Yields:
+    A tuple of name and id.
+  """
+  for match in RESOURCE_EXTRACT_REGEX.finditer(string_to_scan):
+    yield match.group(1, 2)
+
+
+def _ReadOrderedResourceIds(path):
+  """Reads ordered resource ids from the given file.
+
+  The resources are expected to be of the format produced by running Chrome
+  with --print-resource-ids command line.
+
+  Args:
+    path: File path to read resource ids from.
+
+  Returns:
+    An array of ordered resource ids.
+  """
+  ordered_resource_ids = []
+  with open(path, "r") as f:
+    for match in ORDERED_RESOURCE_IDS_REGEX.finditer(f.read()):
+      ordered_resource_ids.append(int(match.group(1)))
+  return ordered_resource_ids
+
+
+def GenerateResourceMapping(original_resources, ordered_resource_ids):
+  """Generates a resource mapping from the ordered ids and the original mapping.
+
+  The returned dict will assign new ids to ordered_resource_ids numerically
+  increasing from 101.
+
+  Args:
+    original_resources: A dict of original resource ids to resource names.
+    ordered_resource_ids: An array of ordered resource ids.
+
+  Returns:
+    A dict of resource ids to resource names.
+  """
+  output_resource_map = {}
+  # 101 is used as the starting value since other parts of GRIT require it to be
+  # the minimum (e.g. rc_header.py) based on Windows resource numbering.
+  next_id = 101
+  for original_id in ordered_resource_ids:
+    resource_name = original_resources[original_id]
+    output_resource_map[next_id] = resource_name
+    next_id += 1
+  return output_resource_map
+
+
+def ReadResourceIdsFromFile(file, original_resources):
+  """Reads resource ids from a GRIT-produced header file.
+
+  Args:
+    file: File to a GRIT-produced header file to read from.
+    original_resources: Dict of resource ids to resource names to add to.
+  """
+  for resource_name, resource_id in _GetResourceNameIdPairsIter(file.read()):
+    original_resources[int(resource_id)] = resource_name
+
+
+def _ReadOriginalResourceIds(out_dir):
+  """Reads resource ids from GRIT header files in the specified directory.
+
+  Args:
+    out_dir: A Chrome build output directory (e.g. out/gn) to scan.
+
+  Returns:
+    A dict of resource ids to resource names.
+  """
+  original_resources = {}
+  for root, dirnames, filenames in os.walk(out_dir + '/gen'):
+    for filename in filenames:
+      if filename.endswith(('_resources.h', '_settings.h', '_strings.h')):
+        with open(os.path.join(root, filename), "r") as f:
+          ReadResourceIdsFromFile(f, original_resources)
+  return original_resources
+
+
+def _GeneratePredeterminedIdsFile(ordered_resources_file, out_dir):
+  """Generates a predetermined ids file.
+
+  Args:
+    ordered_resources_file: File path to read ordered resource ids from.
+    out_dir: A Chrome build output directory (e.g. out/gn) to scan.
+
+  Returns:
+    A dict of resource ids to resource names.
+  """
+  original_resources = _ReadOriginalResourceIds(out_dir)
+  ordered_resource_ids = _ReadOrderedResourceIds(ordered_resources_file)
+  output_resource_map = GenerateResourceMapping(original_resources,
+                                                ordered_resource_ids)
+  for res_id in sorted(output_resource_map.keys()):
+    print(output_resource_map[res_id], res_id)
+
+
+def main(argv):
+  if len(argv) != 2:
+    print("usage: gen_predetermined_ids.py <ordered_resources_file> <out_dir>")
+    sys.exit(1)
+  ordered_resources_file, out_dir = argv[0], argv[1]
+  _GeneratePredeterminedIdsFile(ordered_resources_file, out_dir)
+
+
+if '__main__' == __name__:
+  main(sys.argv[1:])
diff --git a/tools/grit/grit/format/gen_predetermined_ids_unittest.py b/tools/grit/grit/format/gen_predetermined_ids_unittest.py
new file mode 100644
index 0000000000..bd0331adb4
--- /dev/null
+++ b/tools/grit/grit/format/gen_predetermined_ids_unittest.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for the gen_predetermined_ids module.'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.format import gen_predetermined_ids
+
+class GenPredeterminedIdsUnittest(unittest.TestCase):
+  def testGenerateResourceMapping(self):
+    original_resources = {200: 'A', 201: 'B', 300: 'C', 350: 'D', 370: 'E'}
+    ordered_resource_ids = [300, 201, 370]
+    mapping = gen_predetermined_ids.GenerateResourceMapping(
+        original_resources, ordered_resource_ids)
+    self.assertEqual({101: 'C', 102: 'B', 103: 'E'}, mapping)
+
+  def testReadResourceIdsFromFile(self):
+    f = StringIO('''
+// This file is automatically generated by GRIT. Do not edit.
+
+#pragma once
+
+#define IDS_BOOKMARKS_NO_ITEMS 12500
+#define IDS_BOOKMARK_BAR_IMPORT_LINK (::ui::WhitelistedResource<12501>(), 12501)
+#define IDS_BOOKMARK_X (::ui::WhitelistedResource<12502>(), 12502)
+''')
+    resources = {}
+    gen_predetermined_ids.ReadResourceIdsFromFile(f, resources)
+    self.assertEqual({12500: 'IDS_BOOKMARKS_OPEN_ALL',
+                      12501: 'IDS_BOOKMARKS_OPEN_ALL_INCOGNITO',
+                      12502: 'IDS_BOOKMARK_X'}, resources)
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/gzip_string.py b/tools/grit/grit/format/gzip_string.py
new file mode 100644
index 0000000000..3cd17185c9
--- /dev/null
+++ b/tools/grit/grit/format/gzip_string.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Provides gzip utilities for strings.
+"""
+
+from __future__ import print_function
+
+import gzip
+import io
+import subprocess
+
+
+def GzipStringRsyncable(data):
+  # Make call to host system's gzip to get access to --rsyncable option. This
+  # option makes updates much smaller - if one line is changed in the resource,
+  # it won't have to push the entire compressed resource with the update.
+  # Instead, --rsyncable breaks the file into small chunks, so that one doesn't
+  # affect the other in compression, and then only that chunk will have to be
+  # updated.
+  gzip_proc = subprocess.Popen(['gzip', '--stdout', '--rsyncable',
+                                '--best', '--no-name'],
+                               stdin=subprocess.PIPE,
+                               stdout=subprocess.PIPE,
+                               stderr=subprocess.PIPE)
+  data, stderr = gzip_proc.communicate(data)
+  if gzip_proc.returncode != 0:
+    raise subprocess.CalledProcessError(gzip_proc.returncode, 'gzip',
+                                        stderr)
+  return data
+
+
+def GzipString(data):
+  # Gzipping using Python's built in gzip: Windows doesn't ship with gzip, and
+  # OSX's gzip does not have an --rsyncable option built in. Although this is
+  # not preferable to --rsyncable, it is an option for the systems that do
+  # not have --rsyncable. If used over GzipStringRsyncable, the primary
+  # difference of this function's compression will be larger updates every time
+  # a compressed resource is changed.
+  gzip_output = io.BytesIO()
+  with gzip.GzipFile(mode='wb', compresslevel=9, fileobj=gzip_output,
+                     mtime=0) as gzip_file:
+    gzip_file.write(data)
+  data = gzip_output.getvalue()
+  gzip_output.close()
+  return data
diff --git a/tools/grit/grit/format/gzip_string_unittest.py b/tools/grit/grit/format/gzip_string_unittest.py
new file mode 100644
index 0000000000..c0cfbe1837
--- /dev/null
+++ b/tools/grit/grit/format/gzip_string_unittest.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# Copyright (c) 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.gzip_string'''
+
+from __future__ import print_function
+
+import gzip
+import io
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit.format import gzip_string
+
+
+class FormatGzipStringUnittest(unittest.TestCase):
+
+  def testGzipStringRsyncable(self):
+    # Can only test the rsyncable version on platforms which support rsyncable,
+    # which at the moment is Linux.
+    if sys.platform == 'linux2':
+      header_begin = (b'\x1f\x8b')  # gzip first two bytes
+      input = (b'TEST STRING STARTING NOW'
+               b'continuing'
+               b'<even more>'
+               b'<finished NOW>')
+
+      compressed = gzip_string.GzipStringRsyncable(input)
+      self.failUnless(header_begin == compressed[:2])
+
+      compressed_file = io.BytesIO()
+      compressed_file.write(compressed)
+      compressed_file.seek(0)
+
+      with gzip.GzipFile(mode='rb', fileobj=compressed_file) as f:
+        output = f.read()
+      self.failUnless(output == input)
+
+  def testGzipString(self):
+    header_begin = b'\x1f\x8b'  # gzip first two bytes
+    input = (b'TEST STRING STARTING NOW'
+             b'continuing'
+             b'<even more>'
+             b'<finished NOW>')
+
+    compressed = gzip_string.GzipString(input)
+    self.failUnless(header_begin == compressed[:2])
+
+    compressed_file = io.BytesIO()
+    compressed_file.write(compressed)
+    compressed_file.seek(0)
+
+    with gzip.GzipFile(mode='rb', fileobj=compressed_file) as f:
+      output = f.read()
+    self.failUnless(output == input)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/html_inline.py b/tools/grit/grit/format/html_inline.py
new file mode 100644
index 0000000000..da55216ea4
--- /dev/null
+++ b/tools/grit/grit/format/html_inline.py
@@ -0,0 +1,602 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Flattens a HTML file by inlining its external resources.
+
+This is a small script that takes a HTML file, looks for src attributes
+and inlines the specified file, producing one HTML file with no external
+dependencies. It recursively inlines the included files.
+"""
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+import base64
+import mimetypes
+
+from grit import lazy_re
+from grit import util
+from grit.format import minifier
+
+# There is a python bug that makes mimetypes crash if the Windows
+# registry contains non-Latin keys ( http://bugs.python.org/issue9291
+# ). Initing manually and blocking external mime-type databases will
+# prevent that bug and if we add svg manually, it will still give us
+# the data we need.
+mimetypes.init([])
+mimetypes.add_type('image/svg+xml', '.svg')
+
+# webm video type is not always available if mimetype package is outdated.
+mimetypes.add_type('video/webm', '.webm')
+
+DIST_DEFAULT = 'chromium'
+DIST_ENV_VAR = 'CHROMIUM_BUILD'
+DIST_SUBSTR = '%DISTRIBUTION%'
+
+# Matches beginning of an "if" block.
+_BEGIN_IF_BLOCK = lazy_re.compile(
+    r'<if [^>]*?expr=("(?P<expr1>[^">]*)"|\'(?P<expr2>[^\'>]*)\')[^>]*?>')
+
+# Matches ending of an "if" block.
+_END_IF_BLOCK = lazy_re.compile(r'</if>')
+
+# Used by DoInline to replace various links with inline content.
+_STYLESHEET_RE = lazy_re.compile(
+    r'<link rel="stylesheet"[^>]+?href="(?P<filename>[^"]*)".*?>(\s*</link>)?',
+    re.DOTALL)
+_INCLUDE_RE = lazy_re.compile(
+    r'(?P<comment>\/\/ )?<include[^>]+?'
+    r'src=("(?P<file1>[^">]*)"|\'(?P<file2>[^\'>]*)\').*?>(\s*</include>)?',
+    re.DOTALL)
+_SRC_RE = lazy_re.compile(
+    r'<(?!script)(?:[^>]+?\s)src="(?!\[\[|{{)(?P<filename>[^"\']*)"',
+    re.MULTILINE)
+# This re matches '<img srcset="..."' or '<source srcset="..."'
+_SRCSET_RE = lazy_re.compile(
+    r'<(img|source)\b(?:[^>]*?\s)srcset="(?!\[\[|{{|\$i18n{)'
+    r'(?P<srcset>[^"\']*)"',
+    re.MULTILINE)
+# This re is for splitting srcset value string into "image candidate strings".
+# Notes:
+# - HTML 5.2 states that URL cannot start or end with comma.
+# - the "descriptor" is either "width descriptor" or "pixel density descriptor".
+#   The first one consists of "valid non-negative integer + letter 'x'",
+#   the second one is formed of "positive valid floating-point number +
+#   letter 'w'". As a reasonable compromise, we match a list of characters
+#   that form both of them.
+# Matches for example "img2.png 2x" or "img9.png 11E-2w".
+_SRCSET_ENTRY_RE = lazy_re.compile(
+    r'\s*(?P<url>[^,\s]\S+[^,\s])'
+    r'(?:\s+(?P<descriptor>[\deE.-]+[wx]))?\s*'
+    r'(?P<separator>,|$)',
+    re.MULTILINE)
+_ICON_RE = lazy_re.compile(
+    r'<link rel="icon"\s(?:[^>]+?\s)?'
+    r'href=(?P<quote>")(?P<filename>[^"\']*)\1',
+    re.MULTILINE)
+
+
+def GetDistribution():
+  """Helper function that gets the distribution we are building.
+
+  Returns:
+    string
+  """
+  distribution = DIST_DEFAULT
+  if DIST_ENV_VAR in os.environ:
+    distribution = os.environ[DIST_ENV_VAR]
+    if len(distribution) > 1 and distribution[0] == '_':
+      distribution = distribution[1:].lower()
+  return distribution
+
+def ConvertFileToDataURL(filename, base_path, distribution, inlined_files,
+    names_only):
+  """Convert filename to inlined data URI.
+
+  Takes a filename from ether "src" or "srcset", and attempts to read the file
+  at 'filename'. Returns data URI as string with given file inlined.
+  If it finds DIST_SUBSTR string in file name, replaces it with distribution.
+  If filename contains ':', it is considered URL and not translated.
+
+  Args:
+    filename: filename string from ether src or srcset attributes.
+    base_path: path that to look for files in
+    distribution: string that should replace DIST_SUBSTR
+    inlined_files: The name of the opened file is appended to this list.
+    names_only: If true, the function will not read the file but just return "".
+                It will still add the filename to |inlined_files|.
+
+  Returns:
+    string
+  """
+  if filename.find(':') != -1:
+    # filename is probably a URL, which we don't want to bother inlining
+    return filename
+
+  filename = filename.replace(DIST_SUBSTR , distribution)
+  filepath = os.path.normpath(os.path.join(base_path, filename))
+  inlined_files.add(filepath)
+
+  if names_only:
+    return ""
+
+  mimetype = mimetypes.guess_type(filename)[0]
+  if mimetype is None:
+    raise Exception('%s is of an an unknown type and '
+                    'cannot be stored in a data url.' % filename)
+  inline_data = base64.standard_b64encode(util.ReadFile(filepath, util.BINARY))
+  return 'data:%s;base64,%s' % (mimetype, inline_data.decode('utf-8'))
+
+
+def SrcInlineAsDataURL(
+    src_match, base_path, distribution, inlined_files, names_only=False,
+    filename_expansion_function=None):
+  """regex replace function.
+
+  Takes a regex match for src="filename", attempts to read the file
+  at 'filename' and returns the src attribute with the file inlined
+  as a data URI. If it finds DIST_SUBSTR string in file name, replaces
+  it with distribution.
+
+  Args:
+    src_match: regex match object with 'filename' named capturing group
+    base_path: path that to look for files in
+    distribution: string that should replace DIST_SUBSTR
+    inlined_files: The name of the opened file is appended to this list.
+    names_only: If true, the function will not read the file but just return "".
+                It will still add the filename to |inlined_files|.
+
+  Returns:
+    string
+  """
+  filename = src_match.group('filename')
+  if filename_expansion_function:
+    filename = filename_expansion_function(filename)
+
+  data_url = ConvertFileToDataURL(filename, base_path, distribution,
+                                  inlined_files, names_only)
+
+  if not data_url:
+    return data_url
+
+  prefix = src_match.string[src_match.start():src_match.start('filename')]
+  suffix = src_match.string[src_match.end('filename'):src_match.end()]
+  return prefix + data_url + suffix
+
+def SrcsetInlineAsDataURL(
+    srcset_match, base_path, distribution, inlined_files, names_only=False,
+    filename_expansion_function=None):
+  """regex replace function to inline files in srcset="..." attributes
+
+  Takes a regex match for srcset="filename 1x, filename 2x, ...", attempts to
+  read the files referenced by filenames and returns the srcset attribute with
+  the files inlined as a data URI. If it finds DIST_SUBSTR string in file name,
+  replaces it with distribution.
+
+  Args:
+    srcset_match: regex match object with 'srcset' named capturing group
+    base_path: path that to look for files in
+    distribution: string that should replace DIST_SUBSTR
+    inlined_files: The name of the opened file is appended to this list.
+    names_only: If true, the function will not read the file but just return "".
+                It will still add the filename to |inlined_files|.
+
+  Returns:
+    string
+  """
+  srcset = srcset_match.group('srcset')
+
+  if not srcset:
+    return srcset_match.group(0)
+
+  # HTML 5.2 defines srcset as a list of "image candidate strings".
+  # Each of them consists of URL and descriptor.
+  # _SRCSET_ENTRY_RE splits srcset into a list of URLs, descriptors and
+  # commas.
+  # The descriptor part will be None if that optional regex didn't match
+  parts = _SRCSET_ENTRY_RE.split(srcset)
+
+  if not parts:
+    return srcset_match.group(0)
+
+  # List of image candidate strings that will form new srcset="..."
+  new_candidates = []
+
+  # When iterating over split srcset we fill this parts of a single image
+  # candidate string: [url, descriptor]
+  candidate = [];
+
+  # Each entry should consist of some text before the entry, the url,
+  # the descriptor or None if the entry has no descriptor, a comma separator or
+  # the end of the line, and finally some text after the entry (which is the
+  # same as the text before the next entry).
+  for i in range(0, len(parts) - 1, 4):
+    before, url, descriptor, separator, after = parts[i:i+5]
+
+    # There must be a comma-separated next entry or this must be the last entry.
+    assert separator == "," or (separator == "" and i == len(parts) - 5), (
+           "Bad srcset format in {}".format(srcset_match.group(0)))
+    # Both before and after the entry must be empty
+    assert before == after == "", (
+           "Bad srcset format in {}".format(srcset_match.group(0)))
+
+    if filename_expansion_function:
+      filename = filename_expansion_function(url)
+    else:
+      filename = url
+
+    data_url = ConvertFileToDataURL(filename, base_path, distribution,
+                                    inlined_files, names_only)
+
+    # This is not "names_only" mode
+    if data_url:
+      candidate = [data_url]
+      if descriptor:
+        candidate.append(descriptor)
+
+      new_candidates.append(" ".join(candidate))
+
+  prefix = srcset_match.string[srcset_match.start():
+      srcset_match.start('srcset')]
+  suffix = srcset_match.string[srcset_match.end('srcset'):srcset_match.end()]
+  return prefix + ','.join(new_candidates) + suffix
+
+class InlinedData:
+  """Helper class holding the results from DoInline().
+
+  Holds the inlined data and the set of filenames of all the inlined
+  files.
+  """
+  def __init__(self, inlined_data, inlined_files):
+    self.inlined_data = inlined_data
+    self.inlined_files = inlined_files
+
+def DoInline(
+    input_filename, grd_node, allow_external_script=False,
+    preprocess_only=False, names_only=False, strip_whitespace=False,
+    rewrite_function=None, filename_expansion_function=None):
+  """Helper function that inlines the resources in a specified file.
+
+  Reads input_filename, finds all the src attributes and attempts to
+  inline the files they are referring to, then returns the result and
+  the set of inlined files.
+
+  Args:
+    input_filename: name of file to read in
+    grd_node: html node from the grd file for this include tag
+    preprocess_only: Skip all HTML processing, only handle <if> and <include>.
+    names_only: |nil| will be returned for the inlined contents (faster).
+    strip_whitespace: remove whitespace and comments in the input files.
+    rewrite_function: function(filepath, text, distribution) which will be
+        called to rewrite html content before inlining images.
+    filename_expansion_function: function(filename) which will be called to
+        rewrite filenames before attempting to read them.
+  Returns:
+    a tuple of the inlined data as a string and the set of filenames
+    of all the inlined files
+  """
+  if filename_expansion_function:
+    input_filename = filename_expansion_function(input_filename)
+  input_filepath = os.path.dirname(input_filename)
+  distribution = GetDistribution()
+
+  # Keep track of all the files we inline.
+  inlined_files = set()
+
+  def SrcReplace(src_match, filepath=input_filepath,
+                 inlined_files=inlined_files):
+    """Helper function to provide SrcInlineAsDataURL with the base file path"""
+    return SrcInlineAsDataURL(
+        src_match, filepath, distribution, inlined_files, names_only=names_only,
+        filename_expansion_function=filename_expansion_function)
+
+  def SrcsetReplace(srcset_match, filepath=input_filepath,
+                 inlined_files=inlined_files):
+    """Helper function to provide SrcsetInlineAsDataURL with the base file
+    path.
+    """
+    return SrcsetInlineAsDataURL(
+        srcset_match, filepath, distribution, inlined_files,
+        names_only=names_only,
+        filename_expansion_function=filename_expansion_function)
+
+  def GetFilepath(src_match, base_path = input_filepath):
+    filename = [v for k, v in src_match.groupdict().items()
+                if k.startswith('file') and v][0]
+
+    if filename.find(':') != -1:
+      # filename is probably a URL, which we don't want to bother inlining
+      return None
+
+    filename = filename.replace('%DISTRIBUTION%', distribution)
+    if filename_expansion_function:
+      filename = filename_expansion_function(filename)
+    return os.path.normpath(os.path.join(base_path, filename))
+
+  def IsConditionSatisfied(src_match):
+    expr1 = src_match.group('expr1') or ''
+    expr2 = src_match.group('expr2') or ''
+    return grd_node is None or grd_node.EvaluateCondition(expr1 + expr2)
+
+  def CheckConditionalElements(str):
+    """Helper function to conditionally inline inner elements"""
+    while True:
+      begin_if = _BEGIN_IF_BLOCK.search(str)
+      if begin_if is None:
+        if _END_IF_BLOCK.search(str) is not None:
+          raise Exception('Unmatched </if>')
+        return str
+
+      condition_satisfied = IsConditionSatisfied(begin_if)
+      leading = str[0:begin_if.start()]
+      content_start = begin_if.end()
+
+      # Find matching "if" block end.
+      count = 1
+      pos = begin_if.end()
+      while True:
+        end_if = _END_IF_BLOCK.search(str, pos)
+        if end_if is None:
+          raise Exception('Unmatched <if>')
+
+        next_if = _BEGIN_IF_BLOCK.search(str, pos)
+        if next_if is None or next_if.start() >= end_if.end():
+          count = count - 1
+          if count == 0:
+            break
+          pos = end_if.end()
+        else:
+          count = count + 1
+          pos = next_if.end()
+
+      content = str[content_start:end_if.start()]
+      trailing = str[end_if.end():]
+
+      if condition_satisfied:
+        str = leading + CheckConditionalElements(content) + trailing
+      else:
+        str = leading + trailing
+
+  def InlineFileContents(src_match,
+                         pattern,
+                         inlined_files=inlined_files,
+                         strip_whitespace=False):
+    """Helper function to inline external files of various types"""
+    filepath = GetFilepath(src_match)
+    if filepath is None:
+      return src_match.group(0)
+    inlined_files.add(filepath)
+
+    if names_only:
+      inlined_files.update(GetResourceFilenames(
+          filepath,
+          grd_node,
+          allow_external_script,
+          rewrite_function,
+          filename_expansion_function=filename_expansion_function))
+      return ""
+    # To recursively save inlined files, we need InlinedData instance returned
+    # by DoInline.
+    inlined_data_inst=DoInline(filepath, grd_node,
+        allow_external_script=allow_external_script,
+        preprocess_only=preprocess_only,
+        strip_whitespace=strip_whitespace,
+        filename_expansion_function=filename_expansion_function)
+
+    inlined_files.update(inlined_data_inst.inlined_files)
+
+    return pattern % inlined_data_inst.inlined_data;
+
+
+  def InlineIncludeFiles(src_match):
+    """Helper function to directly inline generic external files (without
+       wrapping them with any kind of tags).
+    """
+    return InlineFileContents(src_match, '%s')
+
+  def InlineScript(match):
+    """Helper function to inline external script files"""
+    attrs = (match.group('attrs1') + match.group('attrs2')).strip()
+    if attrs:
+      attrs = ' ' + attrs
+    return InlineFileContents(match, '<script' + attrs + '>%s</script>',
+                              strip_whitespace=True)
+
+  def InlineCSSText(text, css_filepath):
+    """Helper function that inlines external resources in CSS text"""
+    filepath = os.path.dirname(css_filepath)
+    # Allow custom modifications before inlining images.
+    if rewrite_function:
+      text = rewrite_function(filepath, text, distribution)
+    text = InlineCSSImages(text, filepath)
+    return InlineCSSImports(text, filepath)
+
+  def InlineCSSFile(src_match, pattern, base_path=input_filepath):
+    """Helper function to inline external CSS files.
+
+    Args:
+      src_match: A regular expression match with a named group named "filename".
+      pattern: The pattern to replace with the contents of the CSS file.
+      base_path: The base path to use for resolving the CSS file.
+
+    Returns:
+      The text that should replace the reference to the CSS file.
+    """
+    filepath = GetFilepath(src_match, base_path)
+    if filepath is None:
+      return src_match.group(0)
+
+    # Even if names_only is set, the CSS file needs to be opened, because it
+    # can link to images that need to be added to the file set.
+    inlined_files.add(filepath)
+
+    # Inline stylesheets included in this css file.
+    text = _INCLUDE_RE.sub(InlineIncludeFiles, util.ReadFile(filepath, 'utf-8'))
+    # When resolving CSS files we need to pass in the path so that relative URLs
+    # can be resolved.
+
+    return pattern % InlineCSSText(text, filepath)
+
+  def GetUrlRegexString(postfix=''):
+    """Helper function that returns a string for a regex that matches url('')
+       but not url([[ ]]) or url({{ }}). Appends |postfix| to group names.
+    """
+    url_re = (r'url\((?!\[\[|{{)(?P<q%s>"|\'|)(?P<filename%s>[^"\'()]*)'
+              r'(?P=q%s)\)')
+    return url_re % (postfix, postfix, postfix)
+
+  def InlineCSSImages(text, filepath=input_filepath):
+    """Helper function that inlines external images in CSS backgrounds."""
+    # Replace contents of url() for css attributes: content, background,
+    # or *-image.
+    property_re = r'(content|background|[\w-]*-image):[^;]*'
+    # Replace group names to prevent duplicates when forming value_re.
+    image_set_value_re = (r'image-set\(([ ]*' + GetUrlRegexString('2') +
+        r'[ ]*[0-9.]*x[ ]*(,[ ]*)?)+\)')
+    value_re = '(%s|%s)' % (GetUrlRegexString(), image_set_value_re)
+    css_re = property_re + value_re
+    return re.sub(css_re, lambda m: InlineCSSUrls(m, filepath), text)
+
+  def InlineCSSUrls(src_match, filepath=input_filepath):
+    """Helper function that inlines each url on a CSS image rule match."""
+    # Replace contents of url() references in matches.
+    return re.sub(GetUrlRegexString(),
+                  lambda m: SrcReplace(m, filepath),
+                  src_match.group(0))
+
+  def InlineCSSImports(text, filepath=input_filepath):
+    """Helper function that inlines CSS files included via the @import
+       directive.
+    """
+    return re.sub(r'@import\s+' + GetUrlRegexString() + r';',
+                  lambda m: InlineCSSFile(m, '%s', filepath),
+                  text)
+
+
+  flat_text = util.ReadFile(input_filename, 'utf-8')
+
+  # Check conditional elements, remove unsatisfied ones from the file. We do
+  # this twice. The first pass is so that we don't even bother calling
+  # InlineScript, InlineCSSFile and InlineIncludeFiles on text we're eventually
+  # going to throw out anyway.
+  flat_text = CheckConditionalElements(flat_text)
+
+  flat_text = _INCLUDE_RE.sub(InlineIncludeFiles, flat_text)
+
+  if not preprocess_only:
+    if strip_whitespace:
+      flat_text = minifier.Minify(flat_text.encode('utf-8'),
+                                  input_filename).decode('utf-8')
+
+    if not allow_external_script:
+      # We need to inline css and js before we inline images so that image
+      # references gets inlined in the css and js
+      flat_text = re.sub(r'<script (?P<attrs1>.*?)src="(?P<filename>[^"\']*)"'
+                         r'(?P<attrs2>.*?)></script>',
+                         InlineScript,
+                         flat_text)
+
+    flat_text = _STYLESHEET_RE.sub(
+        lambda m: InlineCSSFile(m, '<style>%s</style>'),
+        flat_text)
+
+  # Check conditional elements, second pass. This catches conditionals in any
+  # of the text we just inlined.
+  flat_text = CheckConditionalElements(flat_text)
+
+  # Allow custom modifications before inlining images.
+  if rewrite_function:
+    flat_text = rewrite_function(input_filepath, flat_text, distribution)
+
+  if not preprocess_only:
+    flat_text = _SRC_RE.sub(SrcReplace, flat_text)
+    flat_text = _SRCSET_RE.sub(SrcsetReplace, flat_text)
+
+    # TODO(arv): Only do this inside <style> tags.
+    flat_text = InlineCSSImages(flat_text)
+
+    flat_text = _ICON_RE.sub(SrcReplace, flat_text)
+
+  if names_only:
+    flat_text = None  # Will contains garbage if the flag is set anyway.
+  return InlinedData(flat_text, inlined_files)
+
+
+def InlineToString(input_filename, grd_node, preprocess_only = False,
+                   allow_external_script=False, strip_whitespace=False,
+                   rewrite_function=None, filename_expansion_function=None):
+  """Inlines the resources in a specified file and returns it as a string.
+
+  Args:
+    input_filename: name of file to read in
+    grd_node: html node from the grd file for this include tag
+  Returns:
+    the inlined data as a string
+  """
+  try:
+    return DoInline(
+        input_filename,
+        grd_node,
+        preprocess_only=preprocess_only,
+        allow_external_script=allow_external_script,
+        strip_whitespace=strip_whitespace,
+        rewrite_function=rewrite_function,
+        filename_expansion_function=filename_expansion_function).inlined_data
+  except IOError as e:
+    raise Exception("Failed to open %s while trying to flatten %s. (%s)" %
+                    (e.filename, input_filename, e.strerror))
+
+
+def InlineToFile(input_filename, output_filename, grd_node):
+  """Inlines the resources in a specified file and writes it.
+
+  Reads input_filename, finds all the src attributes and attempts to
+  inline the files they are referring to, then writes the result
+  to output_filename.
+
+  Args:
+    input_filename: name of file to read in
+    output_filename: name of file to be written to
+    grd_node: html node from the grd file for this include tag
+  Returns:
+    a set of filenames of all the inlined files
+  """
+  inlined_data = InlineToString(input_filename, grd_node)
+  with open(output_filename, 'wb') as out_file:
+    out_file.write(inlined_data)
+
+
+def GetResourceFilenames(filename,
+                         grd_node,
+                         allow_external_script=False,
+                         rewrite_function=None,
+                         filename_expansion_function=None):
+  """For a grd file, returns a set of all the files that would be inline."""
+  try:
+    return DoInline(
+        filename,
+        grd_node,
+        names_only=True,
+        preprocess_only=False,
+        allow_external_script=allow_external_script,
+        strip_whitespace=False,
+        rewrite_function=rewrite_function,
+        filename_expansion_function=filename_expansion_function).inlined_files
+  except IOError as e:
+    raise Exception("Failed to open %s while trying to flatten %s. (%s)" %
+                    (e.filename, filename, e.strerror))
+
+
+def main():
+  if len(sys.argv) <= 2:
+    print("Flattens a HTML file by inlining its external resources.\n")
+    print("html_inline.py inputfile outputfile")
+  else:
+    InlineToFile(sys.argv[1], sys.argv[2], None)
+
+if __name__ == '__main__':
+  main()
diff --git a/tools/grit/grit/format/html_inline_unittest.py b/tools/grit/grit/format/html_inline_unittest.py
new file mode 100644
index 0000000000..1b11e9e476
--- /dev/null
+++ b/tools/grit/grit/format/html_inline_unittest.py
@@ -0,0 +1,927 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.html_inline'''
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit import util
+from grit.format import html_inline
+
+
+class HtmlInlineUnittest(unittest.TestCase):
+  '''Unit tests for HtmlInline.'''
+
+  def testGetResourceFilenames(self):
+    '''Tests that all included files are returned by GetResourceFilenames.'''
+
+    files = {
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <link rel="stylesheet" href="test.css">
+          <link rel="stylesheet"
+              href="really-long-long-long-long-long-test.css">
+        </head>
+        <body>
+          <include src='test.html'>
+          <include
+              src="really-long-long-long-long-long-test-file-omg-so-long.html">
+        </body>
+      </html>
+      ''',
+
+      'test.html': '''
+      <include src="test2.html">
+      ''',
+
+      'really-long-long-long-long-long-test-file-omg-so-long.html': '''
+      <!-- This really long named resource should be included. -->
+      ''',
+
+      'test2.html': '''
+      <!-- This second level resource should also be included. -->
+      ''',
+
+      'test.css': '''
+      .image {
+        background: url('test.png');
+      }
+      ''',
+
+      'really-long-long-long-long-long-test.css': '''
+      a:hover {
+        font-weight: bold;  /* Awesome effect is awesome! */
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+    }
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    resources = html_inline.GetResourceFilenames(tmp_dir.GetPath('index.html'),
+                                                 None)
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    tmp_dir.CleanUp()
+
+  def testUnmatchedEndIfBlock(self):
+    '''Tests that an unmatched </if> raises an exception.'''
+
+    files = {
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <if expr="lang == 'fr'">
+          bonjour
+        </if>
+        <if expr='lang == "de"'>
+          hallo
+        </if>
+        </if>
+      </html>
+      ''',
+    }
+
+    tmp_dir = util.TempDir(files)
+
+    with self.assertRaises(Exception) as cm:
+      html_inline.GetResourceFilenames(tmp_dir.GetPath('index.html'), None)
+    self.failUnlessEqual(str(cm.exception), 'Unmatched </if>')
+    tmp_dir.CleanUp()
+
+  def testCompressedJavaScript(self):
+    '''Tests that ".src=" doesn't treat as a tag.'''
+
+    files = {
+      'index.js': '''
+      if(i<j)a.src="hoge.png";
+      ''',
+    }
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    resources = html_inline.GetResourceFilenames(tmp_dir.GetPath('index.js'),
+                                                 None)
+    resources.add(tmp_dir.GetPath('index.js'))
+    self.failUnlessEqual(resources, source_resources)
+    tmp_dir.CleanUp()
+
+  def testInlineCSSImports(self):
+    '''Tests that @import directives in inlined CSS files are inlined too.
+    '''
+
+    files = {
+      'index.html': '''
+      <html>
+      <head>
+      <link rel="stylesheet" href="css/test.css">
+      </head>
+      </html>
+      ''',
+
+      'css/test.css': '''
+      @import url('test2.css');
+      blink {
+        display: none;
+      }
+      ''',
+
+      'css/test2.css': '''
+      .image {
+        background: url('../images/test.png');
+      }
+      '''.strip(),
+
+      'images/test.png': 'PNG DATA'
+    }
+
+    expected_inlined = '''
+      <html>
+      <head>
+      <style>
+      .image {
+        background: url('data:image/png;base64,UE5HIERBVEE=');
+      }
+      blink {
+        display: none;
+      }
+      </style>
+      </head>
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(util.normpath(filename)))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+
+    tmp_dir.CleanUp()
+
+  def testInlineIgnoresPolymerBindings(self):
+    '''Tests that polymer bindings are ignored when inlining.
+    '''
+
+    files = {
+      'index.html': '''
+      <html>
+      <head>
+      <link rel="stylesheet" href="test.css">
+      </head>
+      <body>
+        <iron-icon src="[[icon]]"></iron-icon><!-- Should be ignored. -->
+        <iron-icon src="{{src}}"></iron-icon><!-- Also ignored. -->
+        <!-- [[image]] should be ignored. -->
+        <div style="background: url([[image]]),
+                                url('test.png');">
+        </div>
+        <div style="background: url('test.png'),
+                                url([[image]]);">
+        </div>
+      </body>
+      </html>
+      ''',
+
+      'test.css': '''
+      .image {
+        background: url('test.png');
+        background-image: url([[ignoreMe]]);
+        background-image: image-set(url({{alsoMe}}), 1x);
+        background-image: image-set(
+            url({{ignore}}) 1x,
+            url('test.png') 2x);
+      }
+      ''',
+
+      'test.png': 'PNG DATA'
+    }
+
+    expected_inlined = '''
+      <html>
+      <head>
+      <style>
+      .image {
+        background: url('data:image/png;base64,UE5HIERBVEE=');
+        background-image: url([[ignoreMe]]);
+        background-image: image-set(url({{alsoMe}}), 1x);
+        background-image: image-set(
+            url({{ignore}}) 1x,
+            url('data:image/png;base64,UE5HIERBVEE=') 2x);
+      }
+      </style>
+      </head>
+      <body>
+        <iron-icon src="[[icon]]"></iron-icon><!-- Should be ignored. -->
+        <iron-icon src="{{src}}"></iron-icon><!-- Also ignored. -->
+        <!-- [[image]] should be ignored. -->
+        <div style="background: url([[image]]),
+                                url('data:image/png;base64,UE5HIERBVEE=');">
+        </div>
+        <div style="background: url('data:image/png;base64,UE5HIERBVEE='),
+                                url([[image]]);">
+        </div>
+      </body>
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(util.normpath(filename)))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+
+    tmp_dir.CleanUp()
+
+  def testInlineCSSWithIncludeDirective(self):
+    '''Tests that include directive in external css files also inlined'''
+
+    files = {
+      'index.html': '''
+      <html>
+      <head>
+      <link rel="stylesheet" href="foo.css">
+      </head>
+      </html>
+      ''',
+
+      'foo.css': '''<include src="style.css">''',
+
+      'style.css': '''
+      <include src="style2.css">
+      blink {
+        display: none;
+      }
+      ''',
+      'style2.css': '''h1 {}''',
+    }
+
+    expected_inlined = '''
+      <html>
+      <head>
+      <style>
+      h1 {}
+      blink {
+        display: none;
+      }
+      </style>
+      </head>
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testCssIncludedFileNames(self):
+    '''Tests that all included files from css are returned'''
+
+    files = {
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <link rel="stylesheet" href="test.css">
+        </head>
+        <body>
+        </body>
+      </html>
+      ''',
+
+      'test.css': '''
+      <include src="test2.css">
+      ''',
+
+      'test2.css': '''
+      <include src="test3.css">
+      .image {
+        background: url('test.png');
+      }
+      ''',
+
+      'test3.css': '''h1 {}''',
+
+      'test.png': 'PNG DATA'
+    }
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    resources = html_inline.GetResourceFilenames(tmp_dir.GetPath('index.html'),
+                                                 None)
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    tmp_dir.CleanUp()
+
+  def testInlineCSSLinks(self):
+    '''Tests that only CSS files referenced via relative URLs are inlined.'''
+
+    files = {
+      'index.html': '''
+      <html>
+      <head>
+      <link rel="stylesheet" href="foo.css">
+      <link rel="stylesheet" href="chrome://resources/bar.css">
+      </head>
+      </html>
+      ''',
+
+      'foo.css': '''
+      @import url(chrome://resources/blurp.css);
+      blink {
+        display: none;
+      }
+      ''',
+    }
+
+    expected_inlined = '''
+      <html>
+      <head>
+      <style>
+      @import url(chrome://resources/blurp.css);
+      blink {
+        display: none;
+      }
+      </style>
+      <link rel="stylesheet" href="chrome://resources/bar.css">
+      </head>
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testFilenameVariableExpansion(self):
+    '''Tests that variables are expanded in filenames before inlining.'''
+
+    files = {
+      'index.html': '''
+      <html>
+      <head>
+      <link rel="stylesheet" href="style[WHICH].css">
+      <script src="script[WHICH].js"></script>
+      </head>
+      <include src="tmpl[WHICH].html">
+      <img src="img[WHICH].png">
+      </html>
+      ''',
+      'style1.css': '''h1 {}''',
+      'tmpl1.html': '''<h1></h1>''',
+      'script1.js': '''console.log('hello');''',
+      'img1.png': '''abc''',
+    }
+
+    expected_inlined = '''
+      <html>
+      <head>
+      <style>h1 {}</style>
+      <script>console.log('hello');</script>
+      </head>
+      <h1></h1>
+      <img src="data:image/png;base64,YWJj">
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    def replacer(var, repl):
+      return lambda filename: filename.replace('[%s]' % var, repl)
+
+    # Test normal inlining.
+    result = html_inline.DoInline(
+        tmp_dir.GetPath('index.html'),
+        None,
+        filename_expansion_function=replacer('WHICH', '1'))
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+
+    # Test names-only inlining.
+    result = html_inline.DoInline(
+        tmp_dir.GetPath('index.html'),
+        None,
+        names_only=True,
+        filename_expansion_function=replacer('WHICH', '1'))
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    tmp_dir.CleanUp()
+
+  def testWithCloseTags(self):
+    '''Tests that close tags are removed.'''
+
+    files = {
+      'index.html': '''
+      <html>
+      <head>
+      <link rel="stylesheet" href="style1.css"></link>
+      <link rel="stylesheet" href="style2.css">
+      </link>
+      <link rel="stylesheet" href="style2.css"
+      >
+      </link>
+      <script src="script1.js"></script>
+      </head>
+      <include src="tmpl1.html"></include>
+      <include src="tmpl2.html">
+      </include>
+      <include src="tmpl2.html"
+      >
+      </include>
+      <img src="img1.png">
+      <include src='single-double-quotes.html"></include>
+      <include src="double-single-quotes.html'></include>
+      </html>
+      ''',
+      'style1.css': '''h1 {}''',
+      'style2.css': '''h2 {}''',
+      'tmpl1.html': '''<h1></h1>''',
+      'tmpl2.html': '''<h2></h2>''',
+      'script1.js': '''console.log('hello');''',
+      'img1.png': '''abc''',
+    }
+
+    expected_inlined = '''
+      <html>
+      <head>
+      <style>h1 {}</style>
+      <style>h2 {}</style>
+      <style>h2 {}</style>
+      <script>console.log('hello');</script>
+      </head>
+      <h1></h1>
+      <h2></h2>
+      <h2></h2>
+      <img src="data:image/png;base64,YWJj">
+      <include src='single-double-quotes.html"></include>
+      <include src="double-single-quotes.html'></include>
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    # Test normal inlining.
+    result = html_inline.DoInline(
+        tmp_dir.GetPath('index.html'),
+        None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testCommentedJsInclude(self):
+    '''Tests that <include> works inside a comment.'''
+
+    files = {
+      'include.js': '// <include src="other.js">',
+      'other.js': '// Copyright somebody\nalert(1);',
+    }
+
+    expected_inlined = '// Copyright somebody\nalert(1);'
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('include.js'), None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('include.js'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testCommentedJsIf(self):
+    '''Tests that <if> works inside a comment.'''
+
+    files = {
+      'if.js': '''
+      // <if expr="True">
+      yep();
+      // </if>
+
+      // <if expr="False">
+      nope();
+      // </if>
+      ''',
+    }
+
+    expected_inlined = '''
+      // 
+      yep();
+      // 
+
+      // 
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    class FakeGrdNode(object):
+      def EvaluateCondition(self, cond):
+        return eval(cond)
+
+    result = html_inline.DoInline(tmp_dir.GetPath('if.js'), FakeGrdNode())
+    resources = result.inlined_files
+
+    resources.add(tmp_dir.GetPath('if.js'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testImgSrcset(self):
+    '''Tests that img srcset="" attributes are converted.'''
+
+    # Note that there is no space before "img10.png" and that
+    # "img11.png" has no descriptor.
+    files = {
+      'index.html': '''
+      <html>
+      <img src="img1.png" srcset="img2.png 1x, img3.png 2x">
+      <img src="img4.png" srcset=" img5.png   1x , img6.png 2x ">
+      <img src="chrome://theme/img11.png" srcset="img7.png 1x, '''\
+          '''chrome://theme/img13.png 2x">
+      <img srcset="img8.png 300w, img9.png 11E-2w,img10.png -1e2w">
+      <img srcset="img11.png">
+      <img srcset="img11.png, img2.png 1x">
+      <img srcset="img2.png 1x, img11.png">
+      </html>
+      ''',
+      'img1.png': '''a1''',
+      'img2.png': '''a2''',
+      'img3.png': '''a3''',
+      'img4.png': '''a4''',
+      'img5.png': '''a5''',
+      'img6.png': '''a6''',
+      'img7.png': '''a7''',
+      'img8.png': '''a8''',
+      'img9.png': '''a9''',
+      'img10.png': '''a10''',
+      'img11.png': '''a11''',
+    }
+
+    expected_inlined = '''
+      <html>
+      <img src="data:image/png;base64,YTE=" srcset="data:image/png;base64,'''\
+          '''YTI= 1x,data:image/png;base64,YTM= 2x">
+      <img src="data:image/png;base64,YTQ=" srcset="data:image/png;base64,'''\
+          '''YTU= 1x,data:image/png;base64,YTY= 2x">
+      <img src="chrome://theme/img11.png" srcset="data:image/png;base64,'''\
+          '''YTc= 1x,chrome://theme/img13.png 2x">
+      <img srcset="data:image/png;base64,YTg= 300w,data:image/png;base64,'''\
+          '''YTk= 11E-2w,data:image/png;base64,YTEw -1e2w">
+      <img srcset="data:image/png;base64,YTEx">
+      <img srcset="data:image/png;base64,YTEx,data:image/png;base64,YTI= 1x">
+      <img srcset="data:image/png;base64,YTI= 1x,data:image/png;base64,YTEx">
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    # Test normal inlining.
+    result = html_inline.DoInline(
+        tmp_dir.GetPath('index.html'),
+        None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testImgSrcsetIgnoresI18n(self):
+    '''Tests that $i18n{...} strings are ignored when inlining.
+    '''
+
+    src_html = '''
+      <html>
+      <head></head>
+      <body>
+        <img srcset="$i18n{foo}">
+      </body>
+      </html>
+      '''
+
+    files = {
+      'index.html': src_html,
+    }
+
+    expected_inlined = src_html
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(util.normpath(filename)))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testSourceSrcset(self):
+    '''Tests that source srcset="" attributes are converted.'''
+
+    # Note that there is no space before "img10.png" and that
+    # "img11.png" has no descriptor.
+    files = {
+      'index.html': '''
+      <html>
+      <source src="img1.png" srcset="img2.png 1x, img3.png 2x">
+      <source src="img4.png" srcset=" img5.png   1x , img6.png 2x ">
+      <source src="chrome://theme/img11.png" srcset="img7.png 1x, '''\
+          '''chrome://theme/img13.png 2x">
+      <source srcset="img8.png 300w, img9.png 11E-2w,img10.png -1e2w">
+      <source srcset="img11.png">
+      </html>
+      ''',
+      'img1.png': '''a1''',
+      'img2.png': '''a2''',
+      'img3.png': '''a3''',
+      'img4.png': '''a4''',
+      'img5.png': '''a5''',
+      'img6.png': '''a6''',
+      'img7.png': '''a7''',
+      'img8.png': '''a8''',
+      'img9.png': '''a9''',
+      'img10.png': '''a10''',
+      'img11.png': '''a11''',
+    }
+
+    expected_inlined = '''
+      <html>
+      <source src="data:image/png;base64,YTE=" srcset="data:image/png;'''\
+          '''base64,YTI= 1x,data:image/png;base64,YTM= 2x">
+      <source src="data:image/png;base64,YTQ=" srcset="data:image/png;'''\
+          '''base64,YTU= 1x,data:image/png;base64,YTY= 2x">
+      <source src="chrome://theme/img11.png" srcset="data:image/png;'''\
+          '''base64,YTc= 1x,chrome://theme/img13.png 2x">
+      <source srcset="data:image/png;base64,YTg= 300w,data:image/png;'''\
+          '''base64,YTk= 11E-2w,data:image/png;base64,YTEw -1e2w">
+      <source srcset="data:image/png;base64,YTEx">
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    # Test normal inlining.
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+    self.failUnlessEqual(expected_inlined,
+                         util.FixLineEnd(result.inlined_data, '\n'))
+    tmp_dir.CleanUp()
+
+  def testConditionalInclude(self):
+    '''Tests that output and dependency generation includes only files not'''\
+        ''' blocked by  <if> macros.'''
+
+    files = {
+      'index.html': '''
+      <html>
+      <if expr="True">
+        <img src="img1.png" srcset="img2.png 1x, img3.png 2x">
+      </if>
+      <if expr="False">
+        <img src="img4.png" srcset=" img5.png 1x, img6.png 2x ">
+      </if>
+      <if expr="True">
+        <img src="chrome://theme/img11.png" srcset="img7.png 1x, '''\
+            '''chrome://theme/img13.png 2x">
+      </if>
+      <img srcset="img8.png 300w, img9.png 11E-2w,img10.png -1e2w">
+      </html>
+      ''',
+      'img1.png': '''a1''',
+      'img2.png': '''a2''',
+      'img3.png': '''a3''',
+      'img4.png': '''a4''',
+      'img5.png': '''a5''',
+      'img6.png': '''a6''',
+      'img7.png': '''a7''',
+      'img8.png': '''a8''',
+      'img9.png': '''a9''',
+      'img10.png': '''a10''',
+    }
+
+    expected_inlined = '''
+      <html>
+      <img src="data:image/png;base64,YTE=" srcset="data:image/png;base64,'''\
+          '''YTI= 1x,data:image/png;base64,YTM= 2x">
+      <img src="chrome://theme/img11.png" srcset="data:image/png;base64,'''\
+          '''YTc= 1x,chrome://theme/img13.png 2x">
+      <img srcset="data:image/png;base64,YTg= 300w,data:image/png;base64,'''\
+          '''YTk= 11E-2w,data:image/png;base64,YTEw -1e2w">
+      </html>
+      '''
+
+    expected_files = [
+      'index.html',
+      'img1.png',
+      'img2.png',
+      'img3.png',
+      'img7.png',
+      'img8.png',
+      'img9.png',
+      'img10.png'
+    ]
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    for filename in expected_files:
+      source_resources.add(tmp_dir.GetPath(filename))
+
+    class FakeGrdNode(object):
+      def EvaluateCondition(self, cond):
+        return eval(cond)
+
+    # Test normal inlining.
+    result = html_inline.DoInline(
+        tmp_dir.GetPath('index.html'),
+        FakeGrdNode())
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+
+    # ignore whitespace
+    expected_inlined = re.sub(r'\s+', ' ', expected_inlined)
+    actually_inlined = re.sub(r'\s+', ' ',
+                              util.FixLineEnd(result.inlined_data, '\n'))
+    self.failUnlessEqual(expected_inlined, actually_inlined);
+    tmp_dir.CleanUp()
+
+  def testPreprocessOnlyEvaluatesIncludeAndIf(self):
+    '''Tests that preprocess_only=true evaluates <include> and <if> only.  '''
+
+    files = {
+      'index.html': '''
+      <html>
+        <head>
+          <link rel="stylesheet" href="not_inlined.css">
+          <script src="also_not_inlined.js">
+        </head>
+        <body>
+          <include src="inline_this.html">
+          <if expr="True">
+            <p>'if' should be evaluated.</p>
+          </if>
+        </body>
+      </html>
+      ''',
+      'not_inlined.css': ''' /* <link> should not be inlined. */ ''',
+      'also_not_inlined.js': ''' // <script> should not be inlined. ''',
+      'inline_this.html': ''' <p>'include' should be inlined.</p> '''
+    }
+
+    expected_inlined = '''
+      <html>
+        <head>
+          <link rel="stylesheet" href="not_inlined.css">
+          <script src="also_not_inlined.js">
+        </head>
+        <body>
+          <p>'include' should be inlined.</p>
+          <p>'if' should be evaluated.</p>
+        </body>
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    source_resources.add(tmp_dir.GetPath('index.html'))
+    source_resources.add(tmp_dir.GetPath('inline_this.html'))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None,
+                                  preprocess_only=True)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+
+    # Ignore whitespace
+    expected_inlined = re.sub(r'\s+', ' ', expected_inlined)
+    actually_inlined = re.sub(r'\s+', ' ',
+                              util.FixLineEnd(result.inlined_data, '\n'))
+    self.failUnlessEqual(expected_inlined, actually_inlined)
+
+    tmp_dir.CleanUp()
+
+  def testPreprocessOnlyAppliesRecursively(self):
+    '''Tests that preprocess_only=true propagates to included files. '''
+
+    files = {
+      'index.html': '''
+      <html>
+        <include src="outer_include.html">
+      </html>
+      ''',
+      'outer_include.html': '''
+      <include src="inner_include.html">
+      <link rel="stylesheet" href="not_inlined.css">
+      ''',
+      'inner_include.html': ''' <p>This should be inlined in index.html</p> ''',
+      'not_inlined.css': ''' /* This should not be inlined. */ '''
+    }
+
+    expected_inlined = '''
+      <html>
+        <p>This should be inlined in index.html</p>
+        <link rel="stylesheet" href="not_inlined.css">
+      </html>
+      '''
+
+    source_resources = set()
+    tmp_dir = util.TempDir(files)
+    source_resources.add(tmp_dir.GetPath('index.html'))
+    source_resources.add(tmp_dir.GetPath('outer_include.html'))
+    source_resources.add(tmp_dir.GetPath('inner_include.html'))
+
+    result = html_inline.DoInline(tmp_dir.GetPath('index.html'), None,
+                                  preprocess_only=True)
+    resources = result.inlined_files
+    resources.add(tmp_dir.GetPath('index.html'))
+    self.failUnlessEqual(resources, source_resources)
+
+    # Ignore whitespace
+    expected_inlined = re.sub(r'\s+', ' ', expected_inlined)
+    actually_inlined = re.sub(r'\s+', ' ',
+                              util.FixLineEnd(result.inlined_data, '\n'))
+    self.failUnlessEqual(expected_inlined, actually_inlined)
+
+    tmp_dir.CleanUp()
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/minifier.py b/tools/grit/grit/format/minifier.py
new file mode 100644
index 0000000000..1a0ea34e49
--- /dev/null
+++ b/tools/grit/grit/format/minifier.py
@@ -0,0 +1,45 @@
+# Copyright 2016 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Framework for stripping whitespace and comments from resource files"""
+
+from __future__ import print_function
+
+from os import path
+import subprocess
+import sys
+
+import six
+
+__js_minifier = None
+__css_minifier = None
+
+def SetJsMinifier(minifier):
+  global __js_minifier
+  __js_minifier = minifier.split()
+
+def SetCssMinifier(minifier):
+  global __css_minifier
+  __css_minifier = minifier.split()
+
+def Minify(source, filename):
+  """Minify |source| (bytes) from |filename| and return bytes."""
+  file_type = path.splitext(filename)[1]
+  minifier = None
+  if file_type == '.js':
+    minifier = __js_minifier
+  elif file_type == '.css':
+    minifier = __css_minifier
+  if not minifier:
+    return source
+  p = subprocess.Popen(
+      minifier,
+      stdin=subprocess.PIPE,
+      stdout=subprocess.PIPE,
+      stderr=subprocess.PIPE)
+  (stdout, stderr) = p.communicate(source)
+  if p.returncode != 0:
+    print('Minification failed for %s' % filename)
+    print(stderr)
+    sys.exit(p.returncode)
+  return stdout
diff --git a/tools/grit/grit/format/policy_templates_json.py b/tools/grit/grit/format/policy_templates_json.py
new file mode 100644
index 0000000000..2f9330bb9a
--- /dev/null
+++ b/tools/grit/grit/format/policy_templates_json.py
@@ -0,0 +1,26 @@
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Translates policy_templates.json files.
+"""
+
+from __future__ import print_function
+
+from grit.node import structure
+
+
+def Format(root, lang='en', output_dir='.'):
+  policy_json = None
+  for item in root.ActiveDescendants():
+    with item:
+      if (isinstance(item, structure.StructureNode) and
+          item.attrs['type'] == 'policy_template_metafile'):
+        json_text = item.gatherer.Translate(
+            lang,
+            pseudo_if_not_available=item.PseudoIsAllowed(),
+            fallback_to_english=item.ShouldFallbackToEnglish())
+        # We're only expecting one node of this kind.
+        assert not policy_json
+        policy_json = json_text
+  return policy_json
diff --git a/tools/grit/grit/format/policy_templates_json_unittest.py b/tools/grit/grit/format/policy_templates_json_unittest.py
new file mode 100644
index 0000000000..e252c94e2c
--- /dev/null
+++ b/tools/grit/grit/format/policy_templates_json_unittest.py
@@ -0,0 +1,207 @@
+#!/usr/bin/env python
+# coding: utf-8
+# Copyright 2017 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unittest for policy_templates_json.py.
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import grit.extern.tclib
+import tempfile
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit.tool import build
+
+
+class PolicyTemplatesJsonUnittest(unittest.TestCase):
+
+  def testPolicyTranslation(self):
+    # Create test policy_templates.json data.
+    caption = "The main policy"
+    caption_translation = "Die Hauptrichtlinie"
+
+    message = \
+      "Red cabbage stays red cabbage and wedding dress stays wedding dress"
+    message_translation = \
+      "Blaukraut bleibt Blaukraut und Brautkleid bleibt Brautkleid"
+
+    schema_key_description = "Number of users"
+    schema_key_description_translation = "Anzahl der Nutzer"
+
+    policy_json = """
+        {
+          "policy_definitions": [
+            {
+              'name': 'MainPolicy',
+              'type': 'main',
+              'owners': ['foo@bar.com'],
+              'schema': {
+                'properties': {
+                  'default_launch_container': {
+                    'enum': [
+                      'tab',
+                      'window',
+                    ],
+                    'type': 'string',
+                  },
+                  'users_number': {
+                    'description': '''%s''',
+                    'type': 'integer',
+                  },
+                },
+                'type': 'object',
+              },
+              'supported_on': ['chrome_os:29-'],
+              'features': {
+                'can_be_recommended': True,
+                'dynamic_refresh': True,
+              },
+              'example_value': True,
+              'caption': '''%s''',
+              'tags': [],
+              'desc': '''This policy does stuff.'''
+            },
+          ],
+          "policy_atomic_group_definitions": [],
+          "placeholders": [],
+          "messages": {
+            'message_string_id': {
+              'desc': '''The description is removed from the grit output''',
+              'text': '''%s'''
+            }
+          }
+        }""" % (schema_key_description, caption, message)
+
+    # Create translations. The translation IDs are hashed from the English text.
+    caption_id = grit.extern.tclib.GenerateMessageId(caption);
+    message_id = grit.extern.tclib.GenerateMessageId(message);
+    schema_key_description_id = grit.extern.tclib.GenerateMessageId(
+        schema_key_description)
+    policy_xtb = """
+<?xml version="1.0" ?>
+<!DOCTYPE translationbundle>
+<translationbundle lang="de">
+<translation id="%s">%s</translation>
+<translation id="%s">%s</translation>
+<translation id="%s">%s</translation>
+</translationbundle>""" % (caption_id, caption_translation,
+                           message_id, message_translation,
+                           schema_key_description_id,
+                           schema_key_description_translation)
+
+    # Write both to a temp file.
+    tmp_dir_name = tempfile.gettempdir()
+
+    json_file_path = os.path.join(tmp_dir_name, 'test.json')
+    with open(json_file_path, 'w') as f:
+      f.write(policy_json.strip())
+
+    xtb_file_path = os.path.join(tmp_dir_name, 'test.xtb')
+    with open(xtb_file_path, 'w') as f:
+      f.write(policy_xtb.strip())
+
+    # Assemble a test grit tree, similar to policy_templates.grd.
+    grd_text = '''
+    <grit base_dir="." latest_public_release="0" current_release="1" source_lang_id="en">
+      <translations>
+        <file path="%s" lang="de" />
+      </translations>
+      <release seq="1">
+        <structures>
+          <structure name="IDD_POLICY_SOURCE_FILE" file="%s" type="policy_template_metafile" />
+        </structures>
+      </release>
+    </grit>''' % (xtb_file_path, json_file_path)
+    grd_string_io = StringIO(grd_text)
+
+    # Parse the grit tree and load the policies' JSON with a gatherer.
+    grd = grd_reader.Parse(grd_string_io, dir=tmp_dir_name, defines={'_google_chrome': True})
+    grd.SetOutputLanguage('en')
+    grd.RunGatherers()
+
+    # Remove the temp files.
+    os.unlink(xtb_file_path)
+    os.unlink(json_file_path)
+
+    # Run grit with en->de translation.
+    env_lang = 'en'
+    out_lang = 'de'
+    env_defs = {'_google_chrome': '1'}
+
+    grd.SetOutputLanguage(env_lang)
+    grd.SetDefines(env_defs)
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(grd, DummyOutput('policy_templates', out_lang), buf)
+    output = buf.getvalue()
+
+    # Caption and message texts get taken from xtb.
+    # desc is 'translated' to some pseudo-English
+    #   'ThïPïs pôPôlïPïcýPý dôéPôés stüPüff'.
+    expected = u"""{
+  "policy_definitions": [
+    {
+      "caption": "%s",
+      "desc": "Th\xefP\xefs p\xf4P\xf4l\xefP\xefc\xfdP\xfd d\xf4\xe9P\xf4\xe9s st\xfcP\xfcff.",
+      "example_value": true,
+      "features": {"can_be_recommended": true, "dynamic_refresh": true},
+      "name": "MainPolicy",
+      "owners": ["foo@bar.com"],
+      "schema": {
+        "properties": {
+          "default_launch_container": {
+            "enum": [
+              "tab",
+              "window"
+            ],
+            "type": "string"
+          },
+          "users_number": {
+            "description": "%s",
+            "type": "integer"
+          }
+        },
+        "type": "object"
+      },
+      "supported_on": ["chrome_os:29-"],
+      "tags": [],
+      "type": "main"
+    }
+  ],
+  "policy_atomic_group_definitions": [
+  ],
+  "messages": {
+    "message_string_id": {
+      "text": "%s"
+    }
+  }
+
+}""" % (caption_translation, schema_key_description_translation,
+        message_translation)
+    self.assertEqual(expected, output)
+
+
+class DummyOutput(object):
+
+  def __init__(self, type, language):
+    self.type = type
+    self.language = language
+
+  def GetType(self):
+    return self.type
+
+  def GetLanguage(self):
+    return self.language
+
+  def GetOutputFilename(self):
+    return 'hello.gif'
diff --git a/tools/grit/grit/format/rc.py b/tools/grit/grit/format/rc.py
new file mode 100644
index 0000000000..ed32bb809e
--- /dev/null
+++ b/tools/grit/grit/format/rc.py
@@ -0,0 +1,474 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Support for formatting an RC file for compilation.
+'''
+
+from __future__ import print_function
+
+import os
+import re
+from functools import partial
+
+import six
+
+from grit import util
+from grit.node import misc
+
+
+def Format(root, lang='en', output_dir='.'):
+  from grit.node import empty, include, message, structure
+
+  yield _FormatHeader(root, lang, output_dir)
+
+  for item in root.ActiveDescendants():
+    if isinstance(item, empty.MessagesNode):
+      # Write one STRINGTABLE per <messages> container.
+      # This is hacky: it iterates over the children twice.
+      yield 'STRINGTABLE\nBEGIN\n'
+      for subitem in item.ActiveDescendants():
+        if isinstance(subitem, message.MessageNode):
+          with subitem:
+            yield FormatMessage(subitem, lang)
+      yield 'END\n\n'
+    elif isinstance(item, include.IncludeNode):
+      with item:
+        yield FormatInclude(item, lang, output_dir)
+    elif isinstance(item, structure.StructureNode):
+      with item:
+        yield FormatStructure(item, lang, output_dir)
+
+
+'''
+This dictionary defines the language charset pair lookup table, which is used
+for replacing the GRIT expand variables for language info in Product Version
+resource. The key is the language ISO country code, and the value
+is the language and character-set pair, which is a hexadecimal string
+consisting of the concatenation of the language and character-set identifiers.
+The first 4 digit of the value is the hex value of LCID, the remaining
+4 digits is the hex value of character-set id(code page)of the language.
+
+LCID resource: http://msdn.microsoft.com/en-us/library/ms776294.aspx
+Codepage resource: http://www.science.co.il/language/locale-codes.asp
+
+We have defined three GRIT expand_variables to be used in the version resource
+file to set the language info. Here is an example how they should be used in
+the VS_VERSION_INFO section of the resource file to allow GRIT to localize
+the language info correctly according to product locale.
+
+VS_VERSION_INFO VERSIONINFO
+...
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+      BLOCK "[GRITVERLANGCHARSETHEX]"
+        BEGIN
+        ...
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", [GRITVERLANGID], [GRITVERCHARSETID]
+    END
+END
+
+'''
+
+_LANGUAGE_CHARSET_PAIR = {
+  # Language neutral LCID, unicode(1200) code page.
+  'neutral'     : '000004b0',
+  # LANG_USER_DEFAULT LCID, unicode(1200) code page.
+  'userdefault' : '040004b0',
+  'ar'          : '040104e8',
+  'fi'          : '040b04e4',
+  'ko'          : '041203b5',
+  'es'          : '0c0a04e4',
+  'bg'          : '040204e3',
+  # No codepage for filipino, use unicode(1200).
+  'fil'         : '046404e4',
+  'fr'          : '040c04e4',
+  'lv'          : '042604e9',
+  'sv'          : '041d04e4',
+  'ca'          : '040304e4',
+  'de'          : '040704e4',
+  'lt'          : '042704e9',
+  # Do not use! This is only around for backwards
+  # compatibility and will be removed - use fil instead
+  'tl'          : '0c0004b0',
+  'zh-CN'       : '080403a8',
+  'zh-TW'       : '040403b6',
+  'zh-HK'       : '0c0403b6',
+  'el'          : '040804e5',
+  'no'          : '001404e4',
+  'nb'          : '041404e4',
+  'nn'          : '081404e4',
+  'th'          : '041e036a',
+  'he'          : '040d04e7',
+  'iw'          : '040d04e7',
+  'pl'          : '041504e2',
+  'tr'          : '041f04e6',
+  'hr'          : '041a04e4',
+  # No codepage for Hindi, use unicode(1200).
+  'hi'          : '043904b0',
+  'pt-PT'       : '081604e4',
+  'pt-BR'       : '041604e4',
+  'uk'          : '042204e3',
+  'cs'          : '040504e2',
+  'hu'          : '040e04e2',
+  'ro'          : '041804e2',
+  # No codepage for Urdu, use unicode(1200).
+  'ur'          : '042004b0',
+  'da'          : '040604e4',
+  'is'          : '040f04e4',
+  'ru'          : '041904e3',
+  'vi'          : '042a04ea',
+  'nl'          : '041304e4',
+  'id'          : '042104e4',
+  'sr'          : '081a04e2',
+  'en-GB'       : '0809040e',
+  'it'          : '041004e4',
+  'sk'          : '041b04e2',
+  'et'          : '042504e9',
+  'ja'          : '041103a4',
+  'sl'          : '042404e2',
+  'en'          : '040904b0',
+  # LCID for Mexico; Windows does not support L.A. LCID.
+  'es-419'      : '080a04e4',
+  # No codepage for Bengali, use unicode(1200).
+  'bn'          : '044504b0',
+  'fa'          : '042904e8',
+  # No codepage for Gujarati, use unicode(1200).
+  'gu'          : '044704b0',
+  # No codepage for Kannada, use unicode(1200).
+  'kn'          : '044b04b0',
+  # Malay (Malaysia) [ms-MY]
+  'ms'          : '043e04e4',
+  # No codepage for Malayalam, use unicode(1200).
+  'ml'          : '044c04b0',
+  # No codepage for Marathi, use unicode(1200).
+  'mr'          : '044e04b0',
+  # No codepage for Oriya , use unicode(1200).
+  'or'          : '044804b0',
+  # No codepage for Tamil, use unicode(1200).
+  'ta'          : '044904b0',
+  # No codepage for Telugu, use unicode(1200).
+  'te'          : '044a04b0',
+  # No codepage for Amharic, use unicode(1200). >= Vista.
+  'am'          : '045e04b0',
+  'sw'          : '044104e4',
+  'af'          : '043604e4',
+  'eu'          : '042d04e4',
+  'fr-CA'       : '0c0c04e4',
+  'gl'          : '045604e4',
+  # No codepage for Zulu, use unicode(1200).
+  'zu'          : '043504b0',
+  'fake-bidi'   : '040d04e7',
+}
+
+# Language ID resource: http://msdn.microsoft.com/en-us/library/ms776294.aspx
+#
+# There is no appropriate sublang for Spanish (Latin America) [es-419], so we
+# use Mexico. SUBLANG_DEFAULT would incorrectly map to Spain. Unlike other
+# Latin American countries, Mexican Spanish is supported by VERSIONINFO:
+# http://msdn.microsoft.com/en-us/library/aa381058.aspx
+
+_LANGUAGE_DIRECTIVE_PAIR = {
+  'neutral'     : 'LANG_NEUTRAL, SUBLANG_NEUTRAL',
+  'userdefault' : 'LANG_NEUTRAL, SUBLANG_DEFAULT',
+  'ar'          : 'LANG_ARABIC, SUBLANG_DEFAULT',
+  'fi'          : 'LANG_FINNISH, SUBLANG_DEFAULT',
+  'ko'          : 'LANG_KOREAN, SUBLANG_KOREAN',
+  'es'          : 'LANG_SPANISH, SUBLANG_SPANISH_MODERN',
+  'bg'          : 'LANG_BULGARIAN, SUBLANG_DEFAULT',
+  # LANG_FILIPINO (100) not in VC 7 winnt.h.
+  'fil'         : '100, SUBLANG_DEFAULT',
+  'fr'          : 'LANG_FRENCH, SUBLANG_FRENCH',
+  'lv'          : 'LANG_LATVIAN, SUBLANG_DEFAULT',
+  'sv'          : 'LANG_SWEDISH, SUBLANG_SWEDISH',
+  'ca'          : 'LANG_CATALAN, SUBLANG_DEFAULT',
+  'de'          : 'LANG_GERMAN, SUBLANG_GERMAN',
+  'lt'          : 'LANG_LITHUANIAN, SUBLANG_LITHUANIAN',
+  # Do not use! See above.
+  'tl'          : 'LANG_NEUTRAL, SUBLANG_DEFAULT',
+  'zh-CN'       : 'LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED',
+  'zh-TW'       : 'LANG_CHINESE, SUBLANG_CHINESE_TRADITIONAL',
+  'zh-HK'       : 'LANG_CHINESE, SUBLANG_CHINESE_HONGKONG',
+  'el'          : 'LANG_GREEK, SUBLANG_DEFAULT',
+  'no'          : 'LANG_NORWEGIAN, SUBLANG_DEFAULT',
+  'nb'          : 'LANG_NORWEGIAN, SUBLANG_NORWEGIAN_BOKMAL',
+  'nn'          : 'LANG_NORWEGIAN, SUBLANG_NORWEGIAN_NYNORSK',
+  'th'          : 'LANG_THAI, SUBLANG_DEFAULT',
+  'he'          : 'LANG_HEBREW, SUBLANG_DEFAULT',
+  'iw'          : 'LANG_HEBREW, SUBLANG_DEFAULT',
+  'pl'          : 'LANG_POLISH, SUBLANG_DEFAULT',
+  'tr'          : 'LANG_TURKISH, SUBLANG_DEFAULT',
+  'hr'          : 'LANG_CROATIAN, SUBLANG_DEFAULT',
+  'hi'          : 'LANG_HINDI, SUBLANG_DEFAULT',
+  'pt-PT'       : 'LANG_PORTUGUESE, SUBLANG_PORTUGUESE',
+  'pt-BR'       : 'LANG_PORTUGUESE, SUBLANG_DEFAULT',
+  'uk'          : 'LANG_UKRAINIAN, SUBLANG_DEFAULT',
+  'cs'          : 'LANG_CZECH, SUBLANG_DEFAULT',
+  'hu'          : 'LANG_HUNGARIAN, SUBLANG_DEFAULT',
+  'ro'          : 'LANG_ROMANIAN, SUBLANG_DEFAULT',
+  'ur'          : 'LANG_URDU, SUBLANG_DEFAULT',
+  'da'          : 'LANG_DANISH, SUBLANG_DEFAULT',
+  'is'          : 'LANG_ICELANDIC, SUBLANG_DEFAULT',
+  'ru'          : 'LANG_RUSSIAN, SUBLANG_DEFAULT',
+  'vi'          : 'LANG_VIETNAMESE, SUBLANG_DEFAULT',
+  'nl'          : 'LANG_DUTCH, SUBLANG_DEFAULT',
+  'id'          : 'LANG_INDONESIAN, SUBLANG_DEFAULT',
+  'sr'          : 'LANG_SERBIAN, SUBLANG_SERBIAN_LATIN',
+  'en-GB'       : 'LANG_ENGLISH, SUBLANG_ENGLISH_UK',
+  'it'          : 'LANG_ITALIAN, SUBLANG_DEFAULT',
+  'sk'          : 'LANG_SLOVAK, SUBLANG_DEFAULT',
+  'et'          : 'LANG_ESTONIAN, SUBLANG_DEFAULT',
+  'ja'          : 'LANG_JAPANESE, SUBLANG_DEFAULT',
+  'sl'          : 'LANG_SLOVENIAN, SUBLANG_DEFAULT',
+  'en'          : 'LANG_ENGLISH, SUBLANG_ENGLISH_US',
+  # No L.A. sublang exists.
+  'es-419'      : 'LANG_SPANISH, SUBLANG_SPANISH_MEXICAN',
+  'bn'          : 'LANG_BENGALI, SUBLANG_DEFAULT',
+  'fa'          : 'LANG_PERSIAN, SUBLANG_DEFAULT',
+  'gu'          : 'LANG_GUJARATI, SUBLANG_DEFAULT',
+  'kn'          : 'LANG_KANNADA, SUBLANG_DEFAULT',
+  'ms'          : 'LANG_MALAY, SUBLANG_DEFAULT',
+  'ml'          : 'LANG_MALAYALAM, SUBLANG_DEFAULT',
+  'mr'          : 'LANG_MARATHI, SUBLANG_DEFAULT',
+  'or'          : 'LANG_ORIYA, SUBLANG_DEFAULT',
+  'ta'          : 'LANG_TAMIL, SUBLANG_DEFAULT',
+  'te'          : 'LANG_TELUGU, SUBLANG_DEFAULT',
+  'am'          : 'LANG_AMHARIC, SUBLANG_DEFAULT',
+  'sw'          : 'LANG_SWAHILI, SUBLANG_DEFAULT',
+  'af'          : 'LANG_AFRIKAANS, SUBLANG_DEFAULT',
+  'eu'          : 'LANG_BASQUE, SUBLANG_DEFAULT',
+  'fr-CA'       : 'LANG_FRENCH, SUBLANG_FRENCH_CANADIAN',
+  'gl'          : 'LANG_GALICIAN, SUBLANG_DEFAULT',
+  'zu'          : 'LANG_ZULU, SUBLANG_DEFAULT',
+  'pa'          : 'LANG_PUNJABI, SUBLANG_PUNJABI_INDIA',
+  'sa'          : 'LANG_SANSKRIT, SUBLANG_SANSKRIT_INDIA',
+  'si'          : 'LANG_SINHALESE, SUBLANG_SINHALESE_SRI_LANKA',
+  'ne'          : 'LANG_NEPALI, SUBLANG_NEPALI_NEPAL',
+  'ti'          : 'LANG_TIGRIGNA, SUBLANG_TIGRIGNA_ERITREA',
+  'fake-bidi'   : 'LANG_HEBREW, SUBLANG_DEFAULT',
+}
+
+# A note on 'no-specific-language' in the following few functions:
+# Some build systems may wish to call GRIT to scan for dependencies in
+# a language-agnostic way, and can then specify this fake language as
+# the output context.  It should never be used when output is actually
+# being generated.
+
+def GetLangCharsetPair(language):
+  if language in _LANGUAGE_CHARSET_PAIR:
+    return _LANGUAGE_CHARSET_PAIR[language]
+  if language != 'no-specific-language':
+    print('Warning:GetLangCharsetPair() found undefined language %s' % language)
+  return ''
+
+def GetLangDirectivePair(language):
+  if language in _LANGUAGE_DIRECTIVE_PAIR:
+    return _LANGUAGE_DIRECTIVE_PAIR[language]
+
+  # We don't check for 'no-specific-language' here because this
+  # function should only get called when output is being formatted,
+  # and at that point we would not want to get
+  # 'no-specific-language' passed as the language.
+  print('Warning:GetLangDirectivePair() found undefined language %s' % language)
+  return 'unknown language: see tools/grit/format/rc.py'
+
+def GetLangIdHex(language):
+  if language in _LANGUAGE_CHARSET_PAIR:
+    langcharset = _LANGUAGE_CHARSET_PAIR[language]
+    lang_id = '0x' + langcharset[0:4]
+    return lang_id
+  if language != 'no-specific-language':
+    print('Warning:GetLangIdHex() found undefined language %s' % language)
+  return ''
+
+
+def GetCharsetIdDecimal(language):
+  if language in _LANGUAGE_CHARSET_PAIR:
+    langcharset = _LANGUAGE_CHARSET_PAIR[language]
+    charset_decimal = int(langcharset[4:], 16)
+    return str(charset_decimal)
+  if language != 'no-specific-language':
+    print('Warning:GetCharsetIdDecimal() found undefined language %s' % language)
+  return ''
+
+
+def GetUnifiedLangCode(language) :
+  r = re.compile('([a-z]{1,2})_([a-z]{1,2})')
+  if r.match(language) :
+    underscore = language.find('_')
+    return language[0:underscore] + '-' + language[underscore + 1:].upper()
+  return language
+
+
+def RcSubstitutions(substituter, lang):
+  '''Add language-based substitutions for Rc files to the substitutor.'''
+  unified_lang_code = GetUnifiedLangCode(lang)
+  substituter.AddSubstitutions({
+      'GRITVERLANGCHARSETHEX': GetLangCharsetPair(unified_lang_code),
+      'GRITVERLANGID': GetLangIdHex(unified_lang_code),
+      'GRITVERCHARSETID': GetCharsetIdDecimal(unified_lang_code)})
+
+
+def _FormatHeader(root, lang, output_dir):
+  '''Returns the required preamble for RC files.'''
+  assert isinstance(lang, six.string_types)
+  assert isinstance(root, misc.GritNode)
+  # Find the location of the resource header file, so that we can include
+  # it.
+  resource_header = 'resource.h'  # fall back to this
+  language_directive = ''
+  for output in root.GetOutputFiles():
+    if output.attrs['type'] == 'rc_header':
+      resource_header = os.path.abspath(output.GetOutputFilename())
+      resource_header = util.MakeRelativePath(output_dir, resource_header)
+    if output.attrs['lang'] != lang:
+      continue
+    if output.attrs['language_section'] == '':
+      # If no language_section is requested, no directive is added
+      # (Used when the generated rc will be included from another rc
+      # file that will have the appropriate language directive)
+      language_directive = ''
+    elif output.attrs['language_section'] == 'neutral':
+      # If a neutral language section is requested (default), add a
+      # neutral language directive
+      language_directive = 'LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL'
+    elif output.attrs['language_section'] == 'lang':
+      language_directive = 'LANGUAGE %s' % GetLangDirectivePair(lang)
+  resource_header = resource_header.replace('\\', '\\\\')
+  return '''// This file is automatically generated by GRIT.  Do not edit.
+
+#include "%s"
+#include <winresrc.h>
+#ifdef IDC_STATIC
+#undef IDC_STATIC
+#endif
+#define IDC_STATIC (-1)
+
+%s
+
+
+''' % (resource_header, language_directive)
+# end _FormatHeader() function
+
+
+def FormatMessage(item, lang):
+  '''Returns a single message of a string table.'''
+  message = item.ws_at_start + item.Translate(lang) + item.ws_at_end
+  # Escape quotation marks (RC format uses doubling-up
+  message = message.replace('"', '""')
+  # Replace linebreaks with a \n escape
+  message = util.LINEBREAKS.sub(r'\\n', message)
+  if hasattr(item.GetRoot(), 'GetSubstituter'):
+    substituter = item.GetRoot().GetSubstituter()
+    message = substituter.Substitute(message)
+
+  name_attr = item.GetTextualIds()[0]
+
+  return '  %-15s "%s"\n' % (name_attr, message)
+
+
+def _FormatSection(item, lang, output_dir):
+  '''Writes out an .rc file section.'''
+  assert isinstance(lang, six.string_types)
+  from grit.node import structure
+  assert isinstance(item, structure.StructureNode)
+
+  if item.IsExcludedFromRc():
+    return ''
+
+  text = item.gatherer.Translate(
+      lang, skeleton_gatherer=item.GetSkeletonGatherer(),
+      pseudo_if_not_available=item.PseudoIsAllowed(),
+      fallback_to_english=item.ShouldFallbackToEnglish()) + '\n\n'
+
+  # Replace the language expand_variables in version rc info.
+  if item.ExpandVariables() and hasattr(item.GetRoot(), 'GetSubstituter'):
+    substituter = item.GetRoot().GetSubstituter()
+    text = substituter.Substitute(text)
+  return text
+
+
+def FormatInclude(item, lang, output_dir, type=None, process_html=False):
+  '''Formats an item that is included in an .rc file (e.g. an ICON).
+
+  Args:
+    item: an IncludeNode or StructureNode
+    lang, output_dir: standard formatter parameters
+    type: .rc file resource type, e.g. 'ICON' (ignored unless item is a
+          StructureNode)
+    process_html: False/True (ignored unless item is a StructureNode)
+  '''
+  assert isinstance(lang, six.string_types)
+  from grit.node import structure
+  from grit.node import include
+  assert isinstance(item, (structure.StructureNode, include.IncludeNode))
+
+  if isinstance(item, include.IncludeNode):
+    type = item.attrs['type'].upper()
+    process_html = item.attrs['flattenhtml'] == 'true'
+    filename_only = item.attrs['filenameonly'] == 'true'
+    relative_path = item.attrs['relativepath'] == 'true'
+  else:
+    assert (isinstance(item, structure.StructureNode) and item.attrs['type'] in
+        ['admin_template', 'chrome_html', 'chrome_scaled_image',
+         'tr_html', 'txt'])
+    filename_only = False
+    relative_path = False
+
+  # By default, we use relative pathnames to included resources so that
+  # sharing the resulting .rc files is possible.
+  #
+  # The FileForLanguage() Function has the side effect of generating the file
+  # if needed (e.g. if it is an HTML file include).
+  file_for_lang = item.FileForLanguage(lang, output_dir)
+  if file_for_lang is None:
+    return ''
+
+  filename = os.path.abspath(file_for_lang)
+  if process_html:
+    filename = item.Process(output_dir)
+  elif filename_only:
+    filename = os.path.basename(filename)
+  elif relative_path:
+    filename = util.MakeRelativePath(output_dir, filename)
+
+  filename = filename.replace('\\', '\\\\')  # escape for the RC format
+
+  if isinstance(item, structure.StructureNode) and item.IsExcludedFromRc():
+    return ''
+
+  name = item.attrs['name']
+  item_id = item.GetRoot().GetIdMap()[name]
+  return '// ID: %d\n%-18s %-18s "%s"\n' % (item_id, name, type, filename)
+
+
+def _DoNotFormat(item, lang, output_dir):
+  return ''
+
+
+# Formatter instance to use for each type attribute
+# when formatting Structure nodes.
+_STRUCTURE_FORMATTERS = {
+  'accelerators'        : _FormatSection,
+  'dialog'              : _FormatSection,
+  'menu'                : _FormatSection,
+  'rcdata'              : _FormatSection,
+  'version'             : _FormatSection,
+  'admin_template'      : partial(FormatInclude, type='ADM'),
+  'chrome_html'         : partial(FormatInclude, type='BINDATA',
+                                                 process_html=True),
+  'chrome_scaled_image' : partial(FormatInclude, type='BINDATA'),
+  'tr_html'             : partial(FormatInclude, type='HTML'),
+  'txt'                 : partial(FormatInclude, type='TXT'),
+  'policy_template_metafile': _DoNotFormat,
+}
+
+
+def FormatStructure(item, lang, output_dir):
+  formatter = _STRUCTURE_FORMATTERS[item.attrs['type']]
+  return formatter(item, lang, output_dir)
diff --git a/tools/grit/grit/format/rc_header.py b/tools/grit/grit/format/rc_header.py
new file mode 100644
index 0000000000..ea2c217f53
--- /dev/null
+++ b/tools/grit/grit/format/rc_header.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Item formatters for RC headers.
+'''
+
+from __future__ import print_function
+
+
+def Format(root, lang='en', output_dir='.'):
+  yield '''\
+// This file is automatically generated by GRIT. Do not edit.
+
+#pragma once
+'''
+  # Check for emit nodes under the rc_header. If any emit node
+  # is present, we assume it means the GRD file wants to override
+  # the default header, with no includes.
+  default_includes = ['#include <atlres.h>', '']
+  emit_lines = []
+  for output_node in root.GetOutputFiles():
+    if output_node.GetType() == 'rc_header':
+      for child in output_node.children:
+        if child.name == 'emit' and child.attrs['emit_type'] == 'prepend':
+          emit_lines.append(child.GetCdata())
+  for line in emit_lines or default_includes:
+    yield line + '\n'
+  if root.IsWhitelistSupportEnabled():
+    yield '#include "ui/base/resource/whitelist.h"\n'
+  for line in FormatDefines(root):
+    yield line
+
+
+def FormatDefines(root):
+  '''Yields #define SYMBOL 1234 lines.
+
+  Args:
+    root: A GritNode.
+  '''
+  tids = root.GetIdMap()
+  rc_header_format = '#define {0} {1}\n'
+  if root.IsWhitelistSupportEnabled():
+    rc_header_format = '#define {0} (::ui::WhitelistedResource<{1}>(), {1})\n'
+  for item in root.ActiveDescendants():
+    with item:
+      for tid in item.GetTextualIds():
+        yield rc_header_format.format(tid, tids[tid])
diff --git a/tools/grit/grit/format/rc_header_unittest.py b/tools/grit/grit/format/rc_header_unittest.py
new file mode 100644
index 0000000000..eed4d70a99
--- /dev/null
+++ b/tools/grit/grit/format/rc_header_unittest.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for the rc_header formatter'''
+
+# GRD samples exceed the 80 character limit.
+# pylint: disable-msg=C6310
+
+from __future__ import print_function
+
+import os
+import sys
+import unittest
+
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from grit import util
+from grit.format import rc_header
+
+
+class RcHeaderFormatterUnittest(unittest.TestCase):
+  def FormatAll(self, grd):
+    output = rc_header.FormatDefines(grd)
+    return ''.join(output).replace(' ', '')
+
+  def testFormatter(self):
+    grd = util.ParseGrdForUnittest('''
+        <includes first_id="300" comment="bingo">
+          <include type="gif" name="ID_LOGO" file="images/logo.gif" />
+        </includes>
+        <messages first_id="10000">
+          <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+            Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+          </message>
+          <message name="IDS_BONGO">
+            Bongo!
+          </message>
+        </messages>
+        <structures>
+          <structure type="dialog" name="IDD_NARROW_DIALOG" file="rc_files/dialogs.rc" />
+          <structure type="version" name="VS_VERSION_INFO" file="rc_files/version.rc" />
+        </structures>''')
+    output = self.FormatAll(grd)
+    self.failUnless(output.count('IDS_GREETING10000'))
+    self.failUnless(output.count('ID_LOGO300'))
+
+  def testOnlyDefineResourcesThatSatisfyOutputCondition(self):
+    grd = util.ParseGrdForUnittest('''
+        <includes first_id="300" comment="bingo">
+          <include type="gif" name="ID_LOGO" file="images/logo.gif" />
+        </includes>
+        <messages first_id="10000">
+          <message name="IDS_FIRSTPRESENTSTRING" desc="Present in .rc file.">
+            I will appear in the .rc file.
+          </message>
+          <if expr="False"> <!--Do not include in the .rc files until used.-->
+            <message name="IDS_MISSINGSTRING" desc="Not present in .rc file.">
+              I will not appear in the .rc file.
+            </message>
+          </if>
+          <if expr="lang != 'es'">
+            <message name="IDS_LANGUAGESPECIFICSTRING" desc="Present in .rc file.">
+              Hello.
+            </message>
+          </if>
+          <if expr="lang == 'es'">
+            <message name="IDS_LANGUAGESPECIFICSTRING" desc="Present in .rc file.">
+              Hola.
+            </message>
+          </if>
+          <message name="IDS_THIRDPRESENTSTRING" desc="Present in .rc file.">
+            I will also appear in the .rc file.
+          </message>
+       </messages>''')
+    output = self.FormatAll(grd)
+    self.failUnless(output.count('IDS_FIRSTPRESENTSTRING10000'))
+    self.failIf(output.count('IDS_MISSINGSTRING'))
+    self.failUnless(output.count('IDS_LANGUAGESPECIFICSTRING10002'))
+    self.failUnless(output.count('IDS_THIRDPRESENTSTRING10003'))
+
+  def testEmit(self):
+    grd = util.ParseGrdForUnittest('''
+        <outputs>
+          <output type="rc_all" filename="dummy">
+            <emit emit_type="prepend">Wrong</emit>
+          </output>
+          <if expr="False">
+            <output type="rc_header" filename="dummy">
+              <emit emit_type="prepend">No</emit>
+            </output>
+          </if>
+          <output type="rc_header" filename="dummy">
+            <emit emit_type="append">Error</emit>
+          </output>
+          <output type="rc_header" filename="dummy">
+            <emit emit_type="prepend">Bingo</emit>
+          </output>
+        </outputs>''')
+    output = ''.join(rc_header.Format(grd, 'en', '.'))
+    output = util.StripBlankLinesAndComments(output)
+    self.assertEqual('#pragma once\nBingo', output)
+
+  def testRcHeaderFormat(self):
+    grd = util.ParseGrdForUnittest('''
+        <includes first_id="300" comment="bingo">
+          <include type="gif" name="IDR_LOGO" file="images/logo.gif" />
+        </includes>
+        <messages first_id="10000">
+          <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+            Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+          </message>
+          <message name="IDS_BONGO">
+            Bongo!
+          </message>
+        </messages>''')
+
+    # Using the default settings.
+    output = rc_header.FormatDefines(grd)
+    self.assertEqual(('#define IDR_LOGO 300\n'
+                      '#define IDS_GREETING 10000\n'
+                      '#define IDS_BONGO 10001\n'), ''.join(output))
+
+    # Using resource whitelist support.
+    grd.SetWhitelistSupportEnabled(True)
+    output = rc_header.FormatDefines(grd)
+    self.assertEqual(('#define IDR_LOGO '
+                      '(::ui::WhitelistedResource<300>(), 300)\n'
+                      '#define IDS_GREETING '
+                      '(::ui::WhitelistedResource<10000>(), 10000)\n'
+                      '#define IDS_BONGO '
+                      '(::ui::WhitelistedResource<10001>(), 10001)\n'),
+                     ''.join(output))
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/rc_unittest.py b/tools/grit/grit/format/rc_unittest.py
new file mode 100644
index 0000000000..d23f063596
--- /dev/null
+++ b/tools/grit/grit/format/rc_unittest.py
@@ -0,0 +1,415 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.rc'''
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import tempfile
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit import util
+from grit.node import structure
+from grit.tool import build
+
+
+_PREAMBLE = '''\
+#include "resource.h"
+#include <winresrc.h>
+#ifdef IDC_STATIC
+#undef IDC_STATIC
+#endif
+#define IDC_STATIC (-1)
+'''
+
+
+class DummyOutput(object):
+  def __init__(self, type, language, file = 'hello.gif'):
+    self.type = type
+    self.language = language
+    self.file = file
+
+  def GetType(self):
+    return self.type
+
+  def GetLanguage(self):
+    return self.language
+
+  def GetOutputFilename(self):
+    return self.file
+
+
+class FormatRcUnittest(unittest.TestCase):
+  def testMessages(self):
+    root = util.ParseGrdForUnittest("""
+      <messages>
+          <message name="IDS_BTN_GO" desc="Button text" meaning="verb">Go!</message>
+          <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+            Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+          </message>
+          <message name="BONGO" desc="Flippo nippo">
+            Howdie "Mr. Elephant", how are you doing?   '''
+          </message>
+          <message name="IDS_WITH_LINEBREAKS">
+Good day sir,
+I am a bee
+Sting sting
+          </message>
+      </messages>
+      """)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+    self.assertEqual(_PREAMBLE + u'''\
+STRINGTABLE
+BEGIN
+  IDS_BTN_GO      "Go!"
+  IDS_GREETING    "Hello %s, how are you doing today?"
+  BONGO           "Howdie ""Mr. Elephant"", how are you doing?   "
+  IDS_WITH_LINEBREAKS "Good day sir,\\nI am a bee\\nSting sting"
+END''', output)
+
+  def testRcSection(self):
+    root = util.ParseGrdForUnittest(r'''
+      <structures>
+          <structure type="menu" name="IDC_KLONKMENU" file="grit\testdata\klonk.rc" encoding="utf-16" />
+          <structure type="dialog" name="IDD_ABOUTBOX" file="grit\testdata\klonk.rc" encoding="utf-16" />
+          <structure type="version" name="VS_VERSION_INFO" file="grit\testdata\klonk.rc" encoding="utf-16" />
+      </structures>''')
+    root.SetOutputLanguage('en')
+    root.RunGatherers()
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+    expected = _PREAMBLE + u'''\
+IDC_KLONKMENU MENU
+BEGIN
+    POPUP "&File"
+    BEGIN
+        MENUITEM "E&xit",                       IDM_EXIT
+        MENUITEM "This be ""Klonk"" me like",   ID_FILE_THISBE
+        POPUP "gonk"
+        BEGIN
+            MENUITEM "Klonk && is [good]",           ID_GONK_KLONKIS
+        END
+    END
+    POPUP "&Help"
+    BEGIN
+        MENUITEM "&About ...",                  IDM_ABOUT
+    END
+END
+
+IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+    ICON            IDI_KLONK,IDC_MYICON,14,9,20,20
+    LTEXT           "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+                    SS_NOPREFIX
+    LTEXT           "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+    DEFPUSHBUTTON   "OK",IDOK,195,6,30,11,WS_GROUP
+    CONTROL         "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+                    BS_AUTORADIOBUTTON,46,51,84,10
+END
+
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,0,0,1
+ PRODUCTVERSION 1,0,0,1
+ FILEFLAGSMASK 0x17L
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x4L
+ FILETYPE 0x1L
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904b0"
+        BEGIN
+            VALUE "FileDescription", "klonk Application"
+            VALUE "FileVersion", "1, 0, 0, 1"
+            VALUE "InternalName", "klonk"
+            VALUE "LegalCopyright", "Copyright (C) 2005"
+            VALUE "OriginalFilename", "klonk.exe"
+            VALUE "ProductName", " klonk Application"
+            VALUE "ProductVersion", "1, 0, 0, 1"
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1200
+    END
+END'''.strip()
+    for expected_line, output_line in zip(expected.split(), output.split()):
+      self.assertEqual(expected_line, output_line)
+
+  def testRcIncludeStructure(self):
+    root = util.ParseGrdForUnittest('''
+      <structures>
+        <structure type="tr_html" name="IDR_HTML" file="bingo.html"/>
+        <structure type="tr_html" name="IDR_HTML2" file="bingo2.html"/>
+      </structures>''', base_dir = '/temp')
+    # We do not run gatherers as it is not needed and wouldn't find the file
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+    expected = (_PREAMBLE +
+                u'IDR_HTML           HTML               "%s"\n'
+                u'IDR_HTML2          HTML               "%s"'
+                % (util.normpath('/temp/bingo.html').replace('\\', '\\\\'),
+                   util.normpath('/temp/bingo2.html').replace('\\', '\\\\')))
+    # hackety hack to work on win32&lin
+    output = re.sub(r'"[c-zC-Z]:', '"', output)
+    self.assertEqual(expected, output)
+
+  def testRcIncludeFile(self):
+    root = util.ParseGrdForUnittest('''
+      <includes>
+        <include type="TXT" name="TEXT_ONE" file="bingo.txt"/>
+        <include type="TXT" name="TEXT_TWO" file="bingo2.txt"  filenameonly="true" />
+      </includes>''', base_dir = '/temp')
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+    expected = (_PREAMBLE +
+                u'TEXT_ONE           TXT                "%s"\n'
+                u'TEXT_TWO           TXT                "%s"'
+                % (util.normpath('/temp/bingo.txt').replace('\\', '\\\\'),
+                   'bingo2.txt'))
+    # hackety hack to work on win32&lin
+    output = re.sub(r'"[c-zC-Z]:', '"', output)
+    self.assertEqual(expected, output)
+
+  def testRcIncludeFlattenedHtmlFile(self):
+    input_file = util.PathFromRoot('grit/testdata/include_test.html')
+    output_file = '%s/HTML_FILE1_include_test.html' % tempfile.gettempdir()
+    root = util.ParseGrdForUnittest('''
+      <includes>
+        <include name="HTML_FILE1" flattenhtml="true" file="%s" type="BINDATA" />
+      </includes>''' % input_file)
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en', output_file),
+                                buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+
+    expected = (_PREAMBLE +
+        u'HTML_FILE1         BINDATA            "HTML_FILE1_include_test.html"')
+    # hackety hack to work on win32&lin
+    output = re.sub(r'"[c-zC-Z]:', '"', output)
+    self.assertEqual(expected, output)
+
+    file_contents = util.ReadFile(output_file, 'utf-8')
+
+    # Check for the content added by the <include> tag.
+    self.failUnless(file_contents.find('Hello Include!') != -1)
+    # Check for the content that was removed by if tag.
+    self.failUnless(file_contents.find('should be removed') == -1)
+    # Check for the content that was kept in place by if.
+    self.failUnless(file_contents.find('should be kept') != -1)
+    self.failUnless(file_contents.find('in the middle...') != -1)
+    self.failUnless(file_contents.find('at the end...') != -1)
+    # Check for nested content that was kept
+    self.failUnless(file_contents.find('nested true should be kept') != -1)
+    self.failUnless(file_contents.find('silbing true should be kept') != -1)
+    # Check for removed "<if>" and "</if>" tags.
+    self.failUnless(file_contents.find('<if expr=') == -1)
+    self.failUnless(file_contents.find('</if>') == -1)
+    os.remove(output_file)
+
+  def testStructureNodeOutputfile(self):
+    input_file = util.PathFromRoot('grit/testdata/simple.html')
+    root = util.ParseGrdForUnittest('''
+        <structures>
+          <structure type="tr_html" name="IDR_HTML" file="%s" />
+        </structures>''' % input_file)
+    struct, = root.GetChildrenOfType(structure.StructureNode)
+    # We must run the gatherer since we'll be wanting the translation of the
+    # file.  The file exists in the location pointed to.
+    root.SetOutputLanguage('en')
+    root.RunGatherers()
+
+    output_dir = tempfile.gettempdir()
+    en_file = struct.FileForLanguage('en', output_dir)
+    self.failUnless(en_file == input_file)
+    fr_file = struct.FileForLanguage('fr', output_dir)
+    self.failUnless(fr_file == os.path.join(output_dir, 'fr_simple.html'))
+
+    contents = util.ReadFile(fr_file, 'utf-8')
+
+    self.failUnless(contents.find('<p>') != -1)  # should contain the markup
+    self.failUnless(contents.find('Hello!') == -1)  # should be translated
+    os.remove(fr_file)
+
+  def testChromeHtmlNodeOutputfile(self):
+    input_file = util.PathFromRoot('grit/testdata/chrome_html.html')
+    output_file = '%s/HTML_FILE1_chrome_html.html' % tempfile.gettempdir()
+    root = util.ParseGrdForUnittest('''
+        <structures>
+          <structure type="chrome_html" name="HTML_FILE1" file="%s" flattenhtml="true" />
+        </structures>''' % input_file)
+    struct, = root.GetChildrenOfType(structure.StructureNode)
+    struct.gatherer.SetDefines({'scale_factors': '2x'})
+    # We must run the gatherers since we'll be wanting the chrome_html output.
+    # The file exists in the location pointed to.
+    root.SetOutputLanguage('en')
+    root.RunGatherers()
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en', output_file),
+                                buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+    expected = (_PREAMBLE +
+        u'HTML_FILE1         BINDATA            "HTML_FILE1_chrome_html.html"')
+    # hackety hack to work on win32&lin
+    output = re.sub(r'"[c-zC-Z]:', '"', output)
+    self.assertEqual(expected, output)
+
+    file_contents = util.ReadFile(output_file, 'utf-8')
+
+    # Check for the content added by the <include> tag.
+    self.failUnless(file_contents.find('Hello Include!') != -1)
+    # Check for inserted -webkit-image-set.
+    self.failUnless(file_contents.find('content: -webkit-image-set') != -1)
+    os.remove(output_file)
+
+  def testSubstitutionHtml(self):
+    input_file = util.PathFromRoot('grit/testdata/toolbar_about.html')
+    root = grd_reader.Parse(StringIO('''<?xml version="1.0" encoding="UTF-8"?>
+      <grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+        <release seq="1" allow_pseudo="False">
+          <structures fallback_to_english="True">
+            <structure type="tr_html" name="IDR_HTML" file="%s" expand_variables="true"/>
+          </structures>
+        </release>
+      </grit>
+      ''' % input_file), util.PathFromRoot('.'))
+    root.SetOutputLanguage('ar')
+    # We must run the gatherers since we'll be wanting the translation of the
+    # file.  The file exists in the location pointed to.
+    root.RunGatherers()
+
+    output_dir = tempfile.gettempdir()
+    struct, = root.GetChildrenOfType(structure.StructureNode)
+    ar_file = struct.FileForLanguage('ar', output_dir)
+    self.failUnless(ar_file == os.path.join(output_dir,
+                                            'ar_toolbar_about.html'))
+
+    contents = util.ReadFile(ar_file, 'utf-8')
+
+    self.failUnless(contents.find('dir="RTL"') != -1)
+    os.remove(ar_file)
+
+  def testFallbackToEnglish(self):
+    root = util.ParseGrdForUnittest(r'''
+        <structures fallback_to_english="True">
+          <structure type="dialog" name="IDD_ABOUTBOX" file="grit\testdata\klonk.rc" encoding="utf-16" />
+        </structures>''', base_dir=util.PathFromRoot('.'))
+    root.SetOutputLanguage('en')
+    root.RunGatherers()
+
+    buf = StringIO()
+    formatter = build.RcBuilder.ProcessNode(
+        root, DummyOutput('rc_all', 'bingobongo'), buf)
+    output = util.StripBlankLinesAndComments(buf.getvalue())
+    self.assertEqual(_PREAMBLE + '''\
+IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+    ICON            IDI_KLONK,IDC_MYICON,14,9,20,20
+    LTEXT           "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+                    SS_NOPREFIX
+    LTEXT           "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+    DEFPUSHBUTTON   "OK",IDOK,195,6,30,11,WS_GROUP
+    CONTROL         "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+                    BS_AUTORADIOBUTTON,46,51,84,10
+END''', output)
+
+
+  def testSubstitutionRc(self):
+    root = grd_reader.Parse(StringIO(r'''<?xml version="1.0" encoding="UTF-8"?>
+    <grit latest_public_release="2" source_lang_id="en-US" current_release="3"
+          base_dir=".">
+      <outputs>
+        <output lang="en" type="rc_all" filename="grit\testdata\klonk_resources.rc"/>
+      </outputs>
+      <release seq="1" allow_pseudo="False">
+        <structures>
+          <structure type="menu" name="IDC_KLONKMENU"
+              file="grit\testdata\klonk.rc" encoding="utf-16"
+              expand_variables="true" />
+        </structures>
+        <messages>
+          <message name="good" sub_variable="true">
+            excellent
+          </message>
+        </messages>
+      </release>
+    </grit>
+    '''), util.PathFromRoot('.'))
+    root.SetOutputLanguage('en')
+    root.RunGatherers()
+
+    buf = StringIO()
+    build.RcBuilder.ProcessNode(root, DummyOutput('rc_all', 'en'), buf)
+    output = buf.getvalue()
+    self.assertEqual('''
+// This file is automatically generated by GRIT.  Do not edit.
+
+#include "resource.h"
+#include <winresrc.h>
+#ifdef IDC_STATIC
+#undef IDC_STATIC
+#endif
+#define IDC_STATIC (-1)
+
+LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL
+
+
+IDC_KLONKMENU MENU
+BEGIN
+    POPUP "&File"
+    BEGIN
+        MENUITEM "E&xit",                       IDM_EXIT
+        MENUITEM "This be ""Klonk"" me like",   ID_FILE_THISBE
+        POPUP "gonk"
+        BEGIN
+            MENUITEM "Klonk && is excellent",           ID_GONK_KLONKIS
+        END
+    END
+    POPUP "&Help"
+    BEGIN
+        MENUITEM "&About ...",                  IDM_ABOUT
+    END
+END
+
+STRINGTABLE
+BEGIN
+  good            "excellent"
+END
+'''.strip(), output.strip())
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/format/resource_map.py b/tools/grit/grit/format/resource_map.py
new file mode 100644
index 0000000000..95a8b83160
--- /dev/null
+++ b/tools/grit/grit/format/resource_map.py
@@ -0,0 +1,159 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''This file contains item formatters for resource_map_header and
+resource_map_source files.  A resource map is a mapping between resource names
+(string) and the internal resource ID.'''
+
+from __future__ import print_function
+
+import os
+from functools import partial
+
+from grit import util
+
+
+def GetFormatter(type):
+  if type == 'resource_map_header':
+    return _FormatHeader
+  if type == 'resource_file_map_source':
+    return partial(_FormatSource, _GetItemPath)
+  if type == 'resource_map_source':
+    return partial(_FormatSource, _GetItemName)
+
+
+def GetMapName(root):
+  '''Get the name of the resource map based on the header file name.  E.g.,
+  if our header filename is theme_resources.h, we name our resource map
+  kThemeResourcesMap.
+
+  |root| is the grd file root.'''
+  outputs = root.GetOutputFiles()
+  rc_header_file = None
+  for output in outputs:
+    if 'rc_header' == output.GetType():
+      rc_header_file = output.GetFilename()
+  if not rc_header_file:
+    raise Exception('unable to find resource header filename')
+  filename = os.path.splitext(os.path.split(rc_header_file)[1])[0]
+  filename = filename[0].upper() + filename[1:]
+  while True:
+    pos = filename.find('_')
+    if pos == -1 or pos >= len(filename):
+      break
+    filename = filename[:pos] + filename[pos + 1].upper() + filename[pos + 2:]
+  return 'k' + filename
+
+
+def _FormatHeader(root, lang='en', output_dir='.'):
+  '''Create the header file for the resource mapping.  This file just declares
+  an array of name/value pairs.'''
+  return '''\
+// This file is automatically generated by GRIT.  Do not edit.
+
+#include <stddef.h>
+
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+  const char* const name;
+  int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+
+extern const GritResourceMap %(map_name)s[];
+extern const size_t %(map_name)sSize;
+''' % { 'map_name': GetMapName(root) }
+
+
+def _FormatSourceHeader(root, output_dir):
+  '''Create the header of the C++ source file for the resource mapping.'''
+  rc_header_file = None
+  map_header_file = None
+  for output in root.GetOutputFiles():
+    type = output.GetType()
+    if 'rc_header' == type:
+      rc_header_file = util.MakeRelativePath(output_dir,
+                                             output.GetOutputFilename())
+    elif 'resource_map_header' == type:
+      map_header_file = util.MakeRelativePath(output_dir,
+                                              output.GetOutputFilename())
+  if not rc_header_file or not map_header_file:
+    raise Exception('resource_map_source output type requires '
+        'a resource_map_header and rc_header outputs')
+  return '''\
+// This file is automatically generated by GRIT.  Do not edit.
+
+#include "%(map_header_file)s"
+
+#include <stddef.h>
+
+#include "base/stl_util.h"
+
+#include "%(rc_header_file)s"
+
+const GritResourceMap %(map_name)s[] = {
+''' % { 'map_header_file': map_header_file,
+        'rc_header_file': rc_header_file,
+        'map_name': GetMapName(root),
+      }
+
+
+def _FormatSourceFooter(root):
+  # Return the footer text.
+  return '''\
+};
+
+const size_t %(map_name)sSize = base::size(%(map_name)s);
+''' % { 'map_name': GetMapName(root) }
+
+
+def _FormatSource(get_key, root, lang, output_dir):
+  from grit.node import include, structure, message
+  id_map = root.GetIdMap()
+  yield _FormatSourceHeader(root, output_dir)
+  seen = set()
+  for item in root.ActiveDescendants():
+    if not item.IsResourceMapSource():
+      continue
+    key = get_key(item)
+    tid = item.attrs['name']
+    if tid not in id_map or key in seen:
+      continue
+    seen.add(key)
+    yield '  {"%s", %s},\n' % (key, tid)
+  yield _FormatSourceFooter(root)
+
+
+def _GetItemName(item):
+  return item.attrs['name']
+
+# Check if |path2| is a subpath of |path1|.
+def _IsSubpath(path1, path2):
+  path1_abs = os.path.abspath(path1)
+  path2_abs = os.path.abspath(path2)
+  common = os.path.commonprefix([path1_abs, path2_abs])
+  return path1_abs == common
+
+def _GetItemPath(item):
+  path = item.GetInputPath().replace("\\", "/")
+
+  # Handle the case where the file resides within the output folder,
+  # by expanding any variables as well as replacing the output folder name with
+  # a fixed string such that the key added to the map does not depend on a given
+  # developer's setup.
+  #
+  # For example this will convert the following path:
+  # ../../out/gchrome/${root_gen_dir}/ui/webui/resources/js/foo.js
+  # to:
+  # @out_folder@/gen/ui/webui/resources/js/foo.js
+
+  real_path = item.ToRealPath(item.GetInputPath())
+  if (item.attrs.get('use_base_dir', 'true') != 'true' and
+          _IsSubpath(os.path.curdir, real_path)):
+    path = os.path.join(
+        '@out_folder@', os.path.relpath(real_path)).replace("\\", "/")
+
+  assert '$' not in path, 'all variables should have been expanded'
+  return path
diff --git a/tools/grit/grit/format/resource_map_unittest.py b/tools/grit/grit/format/resource_map_unittest.py
new file mode 100644
index 0000000000..3499b321ef
--- /dev/null
+++ b/tools/grit/grit/format/resource_map_unittest.py
@@ -0,0 +1,345 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.format.resource_map'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit import util
+from grit.format import resource_map
+
+
+class FormatResourceMapUnittest(unittest.TestCase):
+  def testFormatResourceMap(self):
+    grd = util.ParseGrdForUnittest('''
+        <outputs>
+          <output type="rc_header" filename="the_rc_header.h" />
+          <output type="resource_map_header"
+                  filename="the_resource_map_header.h" />
+        </outputs>
+        <release seq="3">
+          <structures first_id="300">
+            <structure type="menu" name="IDC_KLONKMENU"
+                       file="grit\\testdata\\klonk.rc" encoding="utf-16" />
+          </structures>
+          <includes first_id="10000">
+            <include type="foo" file="abc" name="IDS_FIRSTPRESENT" />
+            <if expr="False">
+              <include type="foo" file="def" name="IDS_MISSING" />
+            </if>
+            <if expr="lang != 'es'">
+              <include type="foo" file="ghi" name="IDS_LANGUAGESPECIFIC" />
+            </if>
+            <if expr="lang == 'es'">
+              <include type="foo" file="jkl" name="IDS_LANGUAGESPECIFIC" />
+            </if>
+            <include type="foo" file="mno" name="IDS_THIRDPRESENT" />
+            <include type="foo" file="opq" name="IDS_FOURTHPRESENT"
+                                   skip_in_resource_map="true" />
+         </includes>
+       </release>''', run_gatherers=True)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+  const char* const name;
+  int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"IDC_KLONKMENU", IDC_KLONKMENU},
+  {"IDS_FIRSTPRESENT", IDS_FIRSTPRESENT},
+  {"IDS_LANGUAGESPECIFIC", IDS_LANGUAGESPECIFIC},
+  {"IDS_THIRDPRESENT", IDS_THIRDPRESENT},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_file_map_source')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"grit/testdata/klonk.rc", IDC_KLONKMENU},
+  {"abc", IDS_FIRSTPRESENT},
+  {"ghi", IDS_LANGUAGESPECIFIC},
+  {"mno", IDS_THIRDPRESENT},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+  def testFormatResourceMapWithGeneratedFile(self):
+    os.environ["root_gen_dir"] = "gen"
+
+    grd = util.ParseGrdForUnittest('''\
+        <outputs>
+          <output type="rc_header" filename="the_rc_header.h" />
+          <output type="resource_map_header"
+                  filename="resource_map_header.h" />
+        </outputs>
+        <release seq="3">
+          <includes first_id="10000">
+            <include type="BINDATA"
+                     file="${root_gen_dir}/foo/bar/baz.js"
+                     name="IDR_FOO_BAR_BAZ_JS"
+                     use_base_dir="false"
+                     compress="gzip" />
+         </includes>
+        </release>''', run_gatherers=True)
+
+    formatter = resource_map.GetFormatter('resource_file_map_source')
+    output = util.StripBlankLinesAndComments(''.join(formatter(grd, 'en', '.')))
+    expected = '''\
+#include "resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"@out_folder@/gen/foo/bar/baz.js", IDR_FOO_BAR_BAZ_JS},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);'''
+    self.assertEqual(expected, output)
+
+  def testFormatResourceMapWithOutputAllEqualsFalseForStructures(self):
+    grd = util.ParseGrdForUnittest('''
+        <outputs>
+          <output type="rc_header" filename="the_rc_header.h" />
+          <output type="resource_map_header"
+                  filename="the_resource_map_header.h" />
+          <output type="resource_map_source"
+                  filename="the_resource_map_header.cc" />
+        </outputs>
+        <release seq="3">
+          <structures first_id="300">
+            <structure type="chrome_scaled_image" name="IDR_KLONKMENU"
+                       file="foo.png" />
+            <if expr="False">
+              <structure type="chrome_scaled_image" name="IDR_MISSING"
+                         file="bar.png" />
+            </if>
+            <if expr="True">
+              <structure type="chrome_scaled_image" name="IDR_BLOB"
+                         file="blob.png" />
+            </if>
+            <if expr="True">
+              <then>
+                <structure type="chrome_scaled_image" name="IDR_METEOR"
+                           file="meteor.png" />
+              </then>
+              <else>
+                <structure type="chrome_scaled_image" name="IDR_METEOR"
+                           file="roetem.png" />
+              </else>
+            </if>
+            <if expr="False">
+              <structure type="chrome_scaled_image" name="IDR_LAST"
+                         file="zyx.png" />
+            </if>
+            <if expr="True">
+              <structure type="chrome_scaled_image" name="IDR_LAST"
+                         file="xyz.png" />
+            </if>
+         </structures>
+        </release>''', run_gatherers=True)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+  const char* const name;
+  int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"IDR_KLONKMENU", IDR_KLONKMENU},
+  {"IDR_BLOB", IDR_BLOB},
+  {"IDR_METEOR", IDR_METEOR},
+  {"IDR_LAST", IDR_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"IDR_KLONKMENU", IDR_KLONKMENU},
+  {"IDR_BLOB", IDR_BLOB},
+  {"IDR_METEOR", IDR_METEOR},
+  {"IDR_LAST", IDR_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+  def testFormatResourceMapWithOutputAllEqualsFalseForIncludes(self):
+    grd = util.ParseGrdForUnittest('''
+        <outputs>
+          <output type="rc_header" filename="the_rc_header.h" />
+          <output type="resource_map_header"
+                  filename="the_resource_map_header.h" />
+        </outputs>
+        <release seq="3">
+          <structures first_id="300">
+            <structure type="menu" name="IDC_KLONKMENU"
+                       file="grit\\testdata\\klonk.rc" encoding="utf-16" />
+          </structures>
+          <includes first_id="10000">
+            <include type="foo" file="abc" name="IDS_FIRSTPRESENT" />
+            <if expr="False">
+              <include type="foo" file="def" name="IDS_MISSING" />
+            </if>
+            <include type="foo" file="mno" name="IDS_THIRDPRESENT" />
+            <if expr="True">
+              <include type="foo" file="blob" name="IDS_BLOB" />
+            </if>
+            <if expr="True">
+              <then>
+                <include type="foo" file="meteor" name="IDS_METEOR" />
+              </then>
+              <else>
+                <include type="foo" file="roetem" name="IDS_METEOR" />
+              </else>
+            </if>
+            <if expr="False">
+              <include type="foo" file="zyx" name="IDS_LAST" />
+            </if>
+            <if expr="True">
+              <include type="foo" file="xyz" name="IDS_LAST" />
+            </if>
+         </includes>
+        </release>''', run_gatherers=True)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+  const char* const name;
+  int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"IDC_KLONKMENU", IDC_KLONKMENU},
+  {"IDS_FIRSTPRESENT", IDS_FIRSTPRESENT},
+  {"IDS_THIRDPRESENT", IDS_THIRDPRESENT},
+  {"IDS_BLOB", IDS_BLOB},
+  {"IDS_METEOR", IDS_METEOR},
+  {"IDS_LAST", IDS_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_file_map_source')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include "the_resource_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"grit/testdata/klonk.rc", IDC_KLONKMENU},
+  {"abc", IDS_FIRSTPRESENT},
+  {"mno", IDS_THIRDPRESENT},
+  {"blob", IDS_BLOB},
+  {"meteor", IDS_METEOR},
+  {"xyz", IDS_LAST},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+  def testFormatStringResourceMap(self):
+    grd = util.ParseGrdForUnittest('''
+        <outputs>
+          <output type="rc_header" filename="the_rc_header.h" />
+          <output type="resource_map_header" filename="the_rc_map_header.h" />
+          <output type="resource_map_source" filename="the_rc_map_source.cc" />
+        </outputs>
+        <release seq="1" allow_pseudo="false">
+          <messages fallback_to_english="true">
+            <message name="IDS_PRODUCT_NAME" desc="The application name">
+              Application
+            </message>
+            <if expr="True">
+              <message name="IDS_DEFAULT_TAB_TITLE_TITLE_CASE"
+                  desc="In Title Case: The default title in a tab.">
+                New Tab
+              </message>
+            </if>
+            <if expr="False">
+              <message name="IDS_DEFAULT_TAB_TITLE"
+                  desc="The default title in a tab.">
+                New tab
+              </message>
+            </if>
+          </messages>
+        </release>''', run_gatherers=True)
+    grd.InitializeIds()
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_header')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include <stddef.h>
+#ifndef GRIT_RESOURCE_MAP_STRUCT_
+#define GRIT_RESOURCE_MAP_STRUCT_
+struct GritResourceMap {
+  const char* const name;
+  int value;
+};
+#endif // GRIT_RESOURCE_MAP_STRUCT_
+extern const GritResourceMap kTheRcHeader[];
+extern const size_t kTheRcHeaderSize;''', output)
+    output = util.StripBlankLinesAndComments(''.join(
+        resource_map.GetFormatter('resource_map_source')(grd, 'en', '.')))
+    self.assertEqual('''\
+#include "the_rc_map_header.h"
+#include <stddef.h>
+#include "base/stl_util.h"
+#include "the_rc_header.h"
+const GritResourceMap kTheRcHeader[] = {
+  {"IDS_PRODUCT_NAME", IDS_PRODUCT_NAME},
+  {"IDS_DEFAULT_TAB_TITLE_TITLE_CASE", IDS_DEFAULT_TAB_TITLE_TITLE_CASE},
+};
+const size_t kTheRcHeaderSize = base::size(kTheRcHeader);''', output)
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/gather/__init__.py b/tools/grit/grit/gather/__init__.py
new file mode 100644
index 0000000000..2d578f5643
--- /dev/null
+++ b/tools/grit/grit/gather/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Module grit.gather
+'''
+
+pass
diff --git a/tools/grit/grit/gather/admin_template.py b/tools/grit/grit/gather/admin_template.py
new file mode 100644
index 0000000000..c26b6a88d7
--- /dev/null
+++ b/tools/grit/grit/gather/admin_template.py
@@ -0,0 +1,62 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Gatherer for administrative template files.
+'''
+
+from __future__ import print_function
+
+import re
+
+from grit.gather import regexp
+from grit import exception
+from grit import lazy_re
+
+
+class MalformedAdminTemplateException(exception.Base):
+  '''This file doesn't look like a .adm file to me.'''
+  pass
+
+
+class AdmGatherer(regexp.RegexpGatherer):
+  '''Gatherer for the translateable portions of an admin template.
+
+  This gatherer currently makes the following assumptions:
+  - there is only one [strings] section and it is always the last section
+    of the file
+  - translateable strings do not need to be escaped.
+  '''
+
+  # Finds the strings section as the group named 'strings'
+  _STRINGS_SECTION = lazy_re.compile(
+      r'(?P<first_part>.+^\[strings\])(?P<strings>.+)\Z',
+      re.MULTILINE | re.DOTALL)
+
+  # Finds the translateable sections from within the [strings] section.
+  _TRANSLATEABLES = lazy_re.compile(
+      r'^\s*[A-Za-z0-9_]+\s*=\s*"(?P<text>.+)"\s*$',
+      re.MULTILINE)
+
+  def Escape(self, text):
+    return text.replace('\n', '\\n')
+
+  def UnEscape(self, text):
+    return text.replace('\\n', '\n')
+
+  def Parse(self):
+    if self.have_parsed_:
+      return
+    self.have_parsed_ = True
+
+    self.text_ = self._LoadInputFile().strip()
+    m = self._STRINGS_SECTION.match(self.text_)
+    if not m:
+      raise MalformedAdminTemplateException()
+    # Add the first part, which is all nontranslateable, to the skeleton
+    self._AddNontranslateableChunk(m.group('first_part'))
+    # Then parse the rest using the _TRANSLATEABLES regexp.
+    self._RegExpParse(self._TRANSLATEABLES, m.group('strings'))
+
+  def GetTextualIds(self):
+    return [self.extkey]
diff --git a/tools/grit/grit/gather/admin_template_unittest.py b/tools/grit/grit/gather/admin_template_unittest.py
new file mode 100644
index 0000000000..c637af3a75
--- /dev/null
+++ b/tools/grit/grit/gather/admin_template_unittest.py
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for the admin template gatherer.'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import admin_template
+from grit import util
+from grit import grd_reader
+from grit import grit_runner
+from grit.tool import build
+
+
+class AdmGathererUnittest(unittest.TestCase):
+  def testParsingAndTranslating(self):
+    pseudofile = StringIO(
+      'bingo bongo\n'
+      'ding dong\n'
+      '[strings] \n'
+      'whatcha="bingo bongo"\n'
+      'gotcha = "bingolabongola "the wise" fingulafongula" \n')
+    gatherer = admin_template.AdmGatherer(pseudofile)
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 2)
+    self.failUnless(gatherer.GetCliques()[1].GetMessage().GetRealContent() ==
+                    'bingolabongola "the wise" fingulafongula')
+
+    translation = gatherer.Translate('en')
+    self.failUnless(translation == gatherer.GetText().strip())
+
+  def testErrorHandling(self):
+    pseudofile = StringIO(
+      'bingo bongo\n'
+      'ding dong\n'
+      'whatcha="bingo bongo"\n'
+      'gotcha = "bingolabongola "the wise" fingulafongula" \n')
+    gatherer = admin_template.AdmGatherer(pseudofile)
+    self.assertRaises(admin_template.MalformedAdminTemplateException,
+                      gatherer.Parse)
+
+  _TRANSLATABLES_FROM_FILE = (
+    'Google', 'Google Desktop', 'Preferences',
+    'Controls Google Desktop preferences',
+    'Indexing and Capture Control',
+    'Controls what files, web pages, and other content will be indexed by Google Desktop.',
+    'Prevent indexing of email',
+    # there are lots more but we don't check any further
+  )
+
+  def VerifyCliquesFromAdmFile(self, cliques):
+    self.failUnless(len(cliques) > 20)
+    for clique, expected in zip(cliques, self._TRANSLATABLES_FROM_FILE):
+      text = clique.GetMessage().GetRealContent()
+      self.failUnless(text == expected)
+
+  def testFromFile(self):
+    fname = util.PathFromRoot('grit/testdata/GoogleDesktop.adm')
+    gatherer = admin_template.AdmGatherer(fname)
+    gatherer.Parse()
+    cliques = gatherer.GetCliques()
+    self.VerifyCliquesFromAdmFile(cliques)
+
+  def MakeGrd(self):
+    grd = grd_reader.Parse(StringIO('''<?xml version="1.0" encoding="UTF-8"?>
+      <grit latest_public_release="2" source_lang_id="en-US" current_release="3">
+        <release seq="3">
+          <structures>
+            <structure type="admin_template" name="IDAT_GOOGLE_DESKTOP_SEARCH"
+              file="GoogleDesktop.adm" exclude_from_rc="true" />
+            <structure type="txt" name="BINGOBONGO"
+              file="README.txt" exclude_from_rc="true" />
+          </structures>
+        </release>
+        <outputs>
+          <output filename="de_res.rc" type="rc_all" lang="de" />
+        </outputs>
+      </grit>'''), util.PathFromRoot('grit/testdata'))
+    grd.SetOutputLanguage('en')
+    grd.RunGatherers()
+    return grd
+
+  def testInGrd(self):
+    grd = self.MakeGrd()
+    cliques = grd.children[0].children[0].children[0].GetCliques()
+    self.VerifyCliquesFromAdmFile(cliques)
+
+  def testFileIsOutput(self):
+    grd = self.MakeGrd()
+    dirname = util.TempDir({})
+    try:
+      tool = build.RcBuilder()
+      tool.o = grit_runner.Options()
+      tool.output_directory = dirname.GetPath()
+      tool.res = grd
+      tool.Process()
+
+      self.failUnless(os.path.isfile(dirname.GetPath('de_GoogleDesktop.adm')))
+      self.failUnless(os.path.isfile(dirname.GetPath('de_README.txt')))
+    finally:
+      dirname.CleanUp()
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/gather/chrome_html.py b/tools/grit/grit/gather/chrome_html.py
new file mode 100644
index 0000000000..71c1332d66
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_html.py
@@ -0,0 +1,377 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Prepares a Chrome HTML file by inlining resources and adding references to
+high DPI resources and removing references to unsupported scale factors.
+
+This is a small gatherer that takes a HTML file, looks for src attributes
+and inlines the specified file, producing one HTML file with no external
+dependencies. It recursively inlines the included files. When inlining CSS
+image files this script also checks for the existence of high DPI versions
+of the inlined file including those on relevant platforms. Unsupported scale
+factors are also removed from existing image sets to support explicitly
+referencing all available images.
+"""
+
+from __future__ import print_function
+
+import os
+import re
+
+from grit import lazy_re
+from grit import util
+from grit.format import html_inline
+from grit.gather import interface
+
+
+# Distribution string to replace with distribution.
+DIST_SUBSTR = '%DISTRIBUTION%'
+
+
+# Matches a chrome theme source URL.
+_THEME_SOURCE = lazy_re.compile(
+    r'(?P<baseurl>chrome://theme/IDR_[A-Z0-9_]*)(?P<query>\?.*)?')
+# Pattern for matching CSS url() function.
+_CSS_URL_PATTERN = r'url\((?P<quote>"|\'|)(?P<filename>[^"\'()]*)(?P=quote)\)'
+# Matches CSS url() functions with the capture group 'filename'.
+_CSS_URL = lazy_re.compile(_CSS_URL_PATTERN)
+# Matches one or more CSS image urls used in given properties.
+_CSS_IMAGE_URLS = lazy_re.compile(
+    r'(?P<attribute>content|background|[\w-]*-image):\s*'
+        r'(?P<urls>(' + _CSS_URL_PATTERN + r'\s*,?\s*)+)')
+# Matches CSS image sets.
+_CSS_IMAGE_SETS = lazy_re.compile(
+    r'(?P<attribute>content|background|[\w-]*-image):[ ]*'
+        r'-webkit-image-set\((?P<images>'
+        r'(\s*,?\s*url\((?P<quote>"|\'|)[^"\'()]*(?P=quote)\)[ ]*[0-9.]*x)*)\)',
+    re.MULTILINE)
+# Matches a single image in a CSS image set with the capture group scale.
+_CSS_IMAGE_SET_IMAGE = lazy_re.compile(r'\s*,?\s*'
+    r'url\((?P<quote>"|\'|)[^"\'()]*(?P=quote)\)[ ]*(?P<scale>[0-9.]*x)',
+    re.MULTILINE)
+_HTML_IMAGE_SRC = lazy_re.compile(
+    r'<img[^>]+src=\"(?P<filename>[^">]*)\"[^>]*>')
+
+def GetImageList(
+    base_path, filename, scale_factors, distribution,
+    filename_expansion_function=None):
+  """Generate the list of images which match the provided scale factors.
+
+  Takes an image filename and checks for files of the same name in folders
+  corresponding to the supported scale factors. If the file is from a
+  chrome://theme/ source, inserts supported @Nx scale factors as high DPI
+  versions.
+
+  Args:
+    base_path: path to look for relative file paths in
+    filename: name of the base image file
+    scale_factors: a list of the supported scale factors (i.e. ['2x'])
+    distribution: string that should replace %DISTRIBUTION%
+
+  Returns:
+    array of tuples containing scale factor and image (i.e.
+        [('1x', 'image.png'), ('2x', '2x/image.png')]).
+  """
+  # Any matches for which a chrome URL handler will serve all scale factors
+  # can simply request all scale factors.
+  theme_match = _THEME_SOURCE.match(filename)
+  if theme_match:
+    images = [('1x', filename)]
+    for scale_factor in scale_factors:
+      scale_filename = "%s@%s" % (theme_match.group('baseurl'), scale_factor)
+      if theme_match.group('query'):
+        scale_filename += theme_match.group('query')
+      images.append((scale_factor, scale_filename))
+    return images
+
+  if filename.find(':') != -1:
+    # filename is probably a URL, only return filename itself.
+    return [('1x', filename)]
+
+  filename = filename.replace(DIST_SUBSTR, distribution)
+  if filename_expansion_function:
+    filename = filename_expansion_function(filename)
+  filepath = os.path.join(base_path, filename)
+  images = [('1x', filename)]
+
+  for scale_factor in scale_factors:
+    # Check for existence of file and add to image set.
+    scale_path = os.path.split(os.path.join(base_path, filename))
+    scale_image_path = os.path.join(scale_path[0], scale_factor, scale_path[1])
+    if os.path.isfile(scale_image_path):
+      # HTML/CSS always uses forward slashed paths.
+      parts = filename.rsplit('/', 1)
+      if len(parts) == 1:
+        path = ''
+      else:
+        path = parts[0] + '/'
+      scale_image_name = path + scale_factor + '/' + parts[-1]
+      images.append((scale_factor, scale_image_name))
+  return images
+
+
+def GenerateImageSet(images, quote):
+  """Generates a -webkit-image-set for the provided list of images.
+
+  Args:
+    images: an array of tuples giving scale factor and file path
+            (i.e. [('1x', 'image.png'), ('2x', '2x/image.png')]).
+    quote: a string giving the quotation character to use (i.e. "'")
+
+  Returns:
+    string giving a -webkit-image-set rule referencing the provided images.
+        (i.e. '-webkit-image-set(url('image.png') 1x, url('2x/image.png') 2x)')
+  """
+  imageset = []
+  for (scale_factor, filename) in images:
+    imageset.append("url(%s%s%s) %s" % (quote, filename, quote, scale_factor))
+  return "-webkit-image-set(%s)" % (', '.join(imageset))
+
+
+def UrlToImageSet(
+    src_match, base_path, scale_factors, distribution,
+    filename_expansion_function=None):
+  """Regex replace function which replaces url() with -webkit-image-set.
+
+  Takes a regex match for url('path'). If the file is local, checks for
+  files of the same name in folders corresponding to the supported scale
+  factors. If the file is from a chrome://theme/ source, inserts the
+  supported @Nx scale factor request. In either case inserts a
+  -webkit-image-set rule to fetch the appropriate image for the current
+  scale factor.
+
+  Args:
+    src_match: regex match object from _CSS_URLS
+    base_path: path to look for relative file paths in
+    scale_factors: a list of the supported scale factors (i.e. ['2x'])
+    distribution: string that should replace %DISTRIBUTION%.
+
+  Returns:
+    string
+  """
+  quote = src_match.group('quote')
+  filename = src_match.group('filename')
+  image_list = GetImageList(
+      base_path, filename, scale_factors, distribution,
+      filename_expansion_function=filename_expansion_function)
+
+  # Don't modify the source if there is only one image.
+  if len(image_list) == 1:
+    return src_match.group(0)
+
+  return GenerateImageSet(image_list, quote)
+
+
+def InsertImageSet(
+    src_match, base_path, scale_factors, distribution,
+    filename_expansion_function=None):
+  """Regex replace function which inserts -webkit-image-set rules.
+
+  Takes a regex match for `property: url('path')[, url('path')]+`.
+  Replaces one or more occurances of the match with image set rules.
+
+  Args:
+    src_match: regex match object from _CSS_IMAGE_URLS
+    base_path: path to look for relative file paths in
+    scale_factors: a list of the supported scale factors (i.e. ['2x'])
+    distribution: string that should replace %DISTRIBUTION%.
+
+  Returns:
+    string
+  """
+  attr = src_match.group('attribute')
+  urls = _CSS_URL.sub(
+      lambda m: UrlToImageSet(m, base_path, scale_factors, distribution,
+                              filename_expansion_function),
+      src_match.group('urls'))
+
+  return "%s: %s" % (attr, urls)
+
+
+def InsertImageStyle(
+    src_match, base_path, scale_factors, distribution,
+    filename_expansion_function=None):
+  """Regex replace function which adds a content style to an <img>.
+
+  Takes a regex match from _HTML_IMAGE_SRC and replaces the attribute with a CSS
+  style which defines the image set.
+  """
+  filename = src_match.group('filename')
+  image_list = GetImageList(
+      base_path, filename, scale_factors, distribution,
+      filename_expansion_function=filename_expansion_function)
+
+  # Don't modify the source if there is only one image or image already defines
+  # a style.
+  if src_match.group(0).find(" style=\"") != -1 or len(image_list) == 1:
+    return src_match.group(0)
+
+  return "%s style=\"content: %s;\">" % (src_match.group(0)[:-1],
+                                        GenerateImageSet(image_list, "'"))
+
+
+def InsertImageSets(
+    filepath, text, scale_factors, distribution,
+    filename_expansion_function=None):
+  """Helper function that adds references to external images available in any of
+  scale_factors in CSS backgrounds.
+  """
+  # Add high DPI urls for css attributes: content, background,
+  # or *-image or <img src="foo">.
+  return _CSS_IMAGE_URLS.sub(
+      lambda m: InsertImageSet(
+          m, filepath, scale_factors, distribution,
+          filename_expansion_function=filename_expansion_function),
+      _HTML_IMAGE_SRC.sub(
+          lambda m: InsertImageStyle(
+              m, filepath, scale_factors, distribution,
+              filename_expansion_function=filename_expansion_function),
+          text))
+
+
+def RemoveImagesNotIn(scale_factors, src_match):
+  """Regex replace function which removes images for scale factors not in
+  scale_factors.
+
+  Takes a regex match for _CSS_IMAGE_SETS. For each image in the group images,
+  checks if this scale factor is in scale_factors and if not, removes it.
+
+  Args:
+    scale_factors: a list of the supported scale factors (i.e. ['1x', '2x'])
+    src_match: regex match object from _CSS_IMAGE_SETS
+
+  Returns:
+    string
+  """
+  attr = src_match.group('attribute')
+  images = _CSS_IMAGE_SET_IMAGE.sub(
+      lambda m: m.group(0) if m.group('scale') in scale_factors else '',
+      src_match.group('images'))
+  return "%s: -webkit-image-set(%s)" % (attr, images)
+
+
+def RemoveImageSetImages(text, scale_factors):
+  """Helper function which removes images in image sets not in the list of
+  supported scale_factors.
+  """
+  return _CSS_IMAGE_SETS.sub(
+      lambda m: RemoveImagesNotIn(scale_factors, m), text)
+
+
+def ProcessImageSets(
+    filepath, text, scale_factors, distribution,
+    filename_expansion_function=None):
+  """Helper function that adds references to external images available in other
+  scale_factors and removes images from image-sets in unsupported scale_factors.
+  """
+  # Explicitly add 1x to supported scale factors so that it is not removed.
+  supported_scale_factors = ['1x']
+  supported_scale_factors.extend(scale_factors)
+  return InsertImageSets(
+      filepath,
+      RemoveImageSetImages(text, supported_scale_factors),
+      scale_factors,
+      distribution,
+      filename_expansion_function=filename_expansion_function)
+
+
+class ChromeHtml(interface.GathererBase):
+  """Represents an HTML document processed for Chrome WebUI.
+
+  HTML documents used in Chrome WebUI have local resources inlined and
+  automatically insert references to high DPI assets used in CSS properties
+  with the use of the -webkit-image-set value. References to unsupported scale
+  factors in image sets are also removed. This does not generate any
+  translateable messages and instead generates a single DataPack resource.
+  """
+
+  def __init__(self, *args, **kwargs):
+    super(ChromeHtml, self).__init__(*args, **kwargs)
+    self.allow_external_script_ = False
+    self.flatten_html_ = False
+    self.preprocess_only_ = False
+    # 1x resources are implicitly already in the source and do not need to be
+    # added.
+    self.scale_factors_ = []
+    self.filename_expansion_function = None
+
+  def SetAttributes(self, attrs):
+    self.allow_external_script_ = ('allowexternalscript' in attrs and
+                                   attrs['allowexternalscript'] == 'true')
+    self.preprocess_only_ = ('preprocess' in attrs and
+                             attrs['preprocess'] == 'true')
+    self.flatten_html_ = (self.preprocess_only_ or ('flattenhtml' in attrs and
+                           attrs['flattenhtml'] == 'true'))
+
+  def SetDefines(self, defines):
+    if 'scale_factors' in defines:
+      self.scale_factors_ = defines['scale_factors'].split(',')
+
+  def GetText(self):
+    """Returns inlined text of the HTML document."""
+    return self.inlined_text_
+
+  def GetTextualIds(self):
+    return [self.extkey]
+
+  def GetData(self, lang, encoding):
+    """Returns inlined text of the HTML document."""
+    ret = self.inlined_text_
+    if encoding == util.BINARY:
+      ret = ret.encode('utf-8')
+    return ret
+
+  def GetHtmlResourceFilenames(self):
+    """Returns a set of all filenames inlined by this file."""
+    if self.flatten_html_:
+      return html_inline.GetResourceFilenames(
+          self.grd_node.ToRealPath(self.GetInputPath()),
+          self.grd_node,
+          allow_external_script=self.allow_external_script_,
+          rewrite_function=lambda fp, t, d: ProcessImageSets(
+              fp, t, self.scale_factors_, d,
+              filename_expansion_function=self.filename_expansion_function),
+          filename_expansion_function=self.filename_expansion_function)
+    return []
+
+  def Translate(self, lang, pseudo_if_not_available=True,
+                skeleton_gatherer=None, fallback_to_english=False):
+    """Returns this document translated."""
+    return self.inlined_text_
+
+  def SetFilenameExpansionFunction(self, fn):
+    self.filename_expansion_function = fn
+
+  def Parse(self):
+    """Parses and inlines the represented file."""
+
+    filename = self.GetInputPath()
+    # If there is a grd_node, prefer its GetInputPath(), as that may do more
+    # processing to make the call to ToRealPath() below work correctly.
+    if self.grd_node:
+      filename = self.grd_node.GetInputPath()
+    if self.filename_expansion_function:
+      filename = self.filename_expansion_function(filename)
+    # Hack: some unit tests supply an absolute path and no root node.
+    if not os.path.isabs(filename):
+      filename = self.grd_node.ToRealPath(filename)
+    if self.flatten_html_:
+      self.inlined_text_ = html_inline.InlineToString(
+          filename,
+          self.grd_node,
+          allow_external_script = self.allow_external_script_,
+          strip_whitespace=True,
+          preprocess_only = self.preprocess_only_,
+          rewrite_function=lambda fp, t, d: ProcessImageSets(
+              fp, t, self.scale_factors_, d,
+              filename_expansion_function=self.filename_expansion_function),
+          filename_expansion_function=self.filename_expansion_function)
+    else:
+      distribution = html_inline.GetDistribution()
+      self.inlined_text_ = ProcessImageSets(
+          os.path.dirname(filename),
+          util.ReadFile(filename, 'utf-8'),
+          self.scale_factors_,
+          distribution,
+          filename_expansion_function=self.filename_expansion_function)
diff --git a/tools/grit/grit/gather/chrome_html_unittest.py b/tools/grit/grit/gather/chrome_html_unittest.py
new file mode 100644
index 0000000000..8c75ee5bf4
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_html_unittest.py
@@ -0,0 +1,610 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.chrome_html'''
+
+from __future__ import print_function
+
+import os
+import re
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from grit import lazy_re
+from grit import util
+from grit.gather import chrome_html
+
+
+_NEW_LINE = lazy_re.compile('(\r\n|\r|\n)', re.MULTILINE)
+
+
+def StandardizeHtml(text):
+  '''Standardizes the newline format and png mime type in Html text.'''
+  return _NEW_LINE.sub('\n', text).replace('data:image/x-png;',
+                                           'data:image/png;')
+
+
+class ChromeHtmlUnittest(unittest.TestCase):
+  '''Unit tests for ChromeHtml.'''
+
+  def testFileResources(self):
+    '''Tests inlined image file resources with available high DPI assets.'''
+
+    tmp_dir = util.TempDir({
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <link rel="stylesheet" href="test.css">
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      ''',
+
+      'test.css': '''
+      .image {
+        background: url('test.png');
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '1.4x/test.png': '1.4x PNG DATA',
+
+      '1.8x/test.png': '1.8x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+    html.SetDefines({'scale_factors': '1.4x,1.8x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <style>
+      .image {
+        background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MS40eCBQTkcgREFUQQ==') 1.4x, url('data:image/png;base64,MS44eCBQTkcgREFUQQ==') 1.8x);
+      }
+      </style>
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesImageTag(self):
+    '''Tests inlined image file resources with available high DPI assets on
+    an image tag.'''
+
+    tmp_dir = util.TempDir({
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <body>
+          <img id="foo" src="test.png">
+        </body>
+      </html>
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      <!DOCTYPE HTML>
+      <html>
+        <body>
+          <img id="foo" src="data:image/png;base64,UE5HIERBVEE=" style="content: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MnggUE5HIERBVEE=') 2x);">
+        </body>
+      </html>
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesNoFlatten(self):
+    '''Tests non-inlined image file resources with available high DPI assets.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url('test.png');
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '1.4x/test.png': '1.4x PNG DATA',
+
+      '1.8x/test.png': '1.8x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '1.4x,1.8x'})
+    html.SetAttributes({'flattenhtml': 'false'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url('test.png') 1x, url('1.4x/test.png') 1.4x, url('1.8x/test.png') 1.8x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesNoFlattenSubdir(self):
+    '''Tests non-inlined image file resources w/high DPI assets in subdirs.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url('sub/test.png');
+      }
+      ''',
+
+      'sub/test.png': 'PNG DATA',
+
+      'sub/1.4x/test.png': '1.4x PNG DATA',
+
+      'sub/1.8x/test.png': '1.8x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '1.4x,1.8x'})
+    html.SetAttributes({'flattenhtml': 'false'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url('sub/test.png') 1x, url('sub/1.4x/test.png') 1.4x, url('sub/1.8x/test.png') 1.8x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesPreprocess(self):
+    '''Tests preprocessed image file resources with available high DPI
+    assets.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url('test.png');
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '1.4x/test.png': '1.4x PNG DATA',
+
+      '1.8x/test.png': '1.8x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '1.4x,1.8x'})
+    html.SetAttributes({'flattenhtml': 'false', 'preprocess': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url('test.png') 1x, url('1.4x/test.png') 1.4x, url('1.8x/test.png') 1.8x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesDoubleQuotes(self):
+    '''Tests inlined image file resources if url() filename is double quoted.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url("test.png");
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url("data:image/png;base64,UE5HIERBVEE=") 1x, url("data:image/png;base64,MnggUE5HIERBVEE=") 2x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesNoQuotes(self):
+    '''Tests inlined image file resources when url() filename is unquoted.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url(test.png);
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesSubdirs(self):
+    '''Tests inlined image file resources if url() filename is in a subdir.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url('some/sub/path/test.png');
+      }
+      ''',
+
+      'some/sub/path/test.png': 'PNG DATA',
+
+      'some/sub/path/2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MnggUE5HIERBVEE=') 2x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesNoFile(self):
+    '''Tests inlined image file resources without available high DPI assets.'''
+
+    tmp_dir = util.TempDir({
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <link rel="stylesheet" href="test.css">
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      ''',
+
+      'test.css': '''
+      .image {
+        background: url('test.png');
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <style>
+      .image {
+        background: url('data:image/png;base64,UE5HIERBVEE=');
+      }
+      </style>
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesMultipleBackgrounds(self):
+    '''Tests inlined image file resources with two url()s.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url(test.png), url(test.png);
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x), -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesMultipleBackgroundsWithNewline1(self):
+    '''Tests inlined image file resources with line break after first url().'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background: url(test.png),
+                    url(test.png);
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x),
+                    -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesMultipleBackgroundsWithNewline2(self):
+    '''Tests inlined image file resources with line break before first url()
+    and before second url().'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background:
+          url(test.png),
+          url(test.png);
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x),
+          -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testFileResourcesCRLF(self):
+    '''Tests inlined image file resource when url() is preceded by a Windows
+    style line break.'''
+
+    tmp_dir = util.TempDir({
+      'test.css': '''
+      .image {
+        background:\r\nurl(test.png);
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      '2x/test.png': '2x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('test.css'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      .image {
+        background: -webkit-image-set(url(data:image/png;base64,UE5HIERBVEE=) 1x, url(data:image/png;base64,MnggUE5HIERBVEE=) 2x);
+      }
+      '''))
+    tmp_dir.CleanUp()
+
+  def testThemeResources(self):
+    '''Tests inserting high DPI chrome://theme references.'''
+
+    tmp_dir = util.TempDir({
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <link rel="stylesheet" href="test.css">
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      ''',
+
+      'test.css': '''
+      .image {
+        background: url('chrome://theme/IDR_RESOURCE_NAME');
+        content: url('chrome://theme/IDR_RESOURCE_NAME_WITH_Q?$1');
+      }
+      ''',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+    html.SetDefines({'scale_factors': '2x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <style>
+      .image {
+        background: -webkit-image-set(url('chrome://theme/IDR_RESOURCE_NAME') 1x, url('chrome://theme/IDR_RESOURCE_NAME@2x') 2x);
+        content: -webkit-image-set(url('chrome://theme/IDR_RESOURCE_NAME_WITH_Q?$1') 1x, url('chrome://theme/IDR_RESOURCE_NAME_WITH_Q@2x?$1') 2x);
+      }
+      </style>
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      '''))
+    tmp_dir.CleanUp()
+
+  def testRemoveUnsupportedScale(self):
+    '''Tests removing an unsupported scale factor from an explicit image-set.'''
+
+    tmp_dir = util.TempDir({
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <link rel="stylesheet" href="test.css">
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      ''',
+
+      'test.css': '''
+      .image {
+        background: -webkit-image-set(url('test.png') 1x,
+                                      url('test1.4.png') 1.4x,
+                                      url('test1.8.png') 1.8x);
+      }
+      ''',
+
+      'test.png': 'PNG DATA',
+
+      'test1.4.png': '1.4x PNG DATA',
+
+      'test1.8.png': '1.8x PNG DATA',
+    })
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+    html.SetDefines({'scale_factors': '1.8x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <style>
+      .image {
+        background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x,
+                                      url('data:image/png;base64,MS44eCBQTkcgREFUQQ==') 1.8x);
+      }
+      </style>
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      '''))
+    tmp_dir.CleanUp()
+
+  def testExpandVariablesInFilename(self):
+    '''
+    Tests variable substitution in filenames while flattening images
+    with multiple scale factors.
+    '''
+
+    tmp_dir = util.TempDir({
+      'index.html': '''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <link rel="stylesheet" href="test.css">
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      ''',
+
+      'test.css': '''
+      .image {
+        background: url('test[WHICH].png');
+      }
+      ''',
+
+      'test1.png': 'PNG DATA',
+      '1.4x/test1.png': '1.4x PNG DATA',
+      '1.8x/test1.png': '1.8x PNG DATA',
+    })
+
+    def replacer(var, repl):
+      return lambda filename: filename.replace('[%s]' % var, repl)
+
+    html = chrome_html.ChromeHtml(tmp_dir.GetPath('index.html'))
+    html.SetDefines({'scale_factors': '1.4x,1.8x'})
+    html.SetAttributes({'flattenhtml': 'true'})
+    html.SetFilenameExpansionFunction(replacer('WHICH', '1'));
+    html.Parse()
+    self.failUnlessEqual(StandardizeHtml(html.GetData('en', 'utf-8')),
+                         StandardizeHtml('''
+      <!DOCTYPE HTML>
+      <html>
+        <head>
+          <style>
+      .image {
+        background: -webkit-image-set(url('data:image/png;base64,UE5HIERBVEE=') 1x, url('data:image/png;base64,MS40eCBQTkcgREFUQQ==') 1.4x, url('data:image/png;base64,MS44eCBQTkcgREFUQQ==') 1.8x);
+      }
+      </style>
+        </head>
+        <body>
+          <!-- Don't need a body. -->
+        </body>
+      </html>
+      '''))
+    tmp_dir.CleanUp()
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/gather/chrome_scaled_image.py b/tools/grit/grit/gather/chrome_scaled_image.py
new file mode 100644
index 0000000000..44f98cbcf0
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_scaled_image.py
@@ -0,0 +1,157 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Gatherer for <structure type="chrome_scaled_image">.
+'''
+
+from __future__ import print_function
+
+import os
+import struct
+
+from grit import exception
+from grit import lazy_re
+from grit import util
+from grit.gather import interface
+
+
+_PNG_SCALE_CHUNK = b'\0\0\0\0csCl\xc1\x30\x60\x4d'
+
+
+def _RescaleImage(data, from_scale, to_scale):
+  if from_scale != to_scale:
+    assert from_scale == 100
+    # Rather than rescaling the image we add a custom chunk directing Chrome to
+    # rescale it on load. Just append it to the PNG data since
+    # _MoveSpecialChunksToFront will move it later anyway.
+    data += _PNG_SCALE_CHUNK
+  return data
+
+
+_PNG_MAGIC = b'\x89PNG\r\n\x1a\n'
+
+'''Mandatory first chunk in order for the png to be valid.'''
+_FIRST_CHUNK = b'IHDR'
+
+'''Special chunks to move immediately after the IHDR chunk. (so that the PNG
+remains valid.)
+'''
+_SPECIAL_CHUNKS = frozenset(b'csCl npTc'.split())
+
+'''Any ancillary chunk not in this list is deleted from the PNG.'''
+_ANCILLARY_CHUNKS_TO_LEAVE = frozenset(
+    b'bKGD cHRM gAMA iCCP pHYs sBIT sRGB tRNS acTL fcTL fdAT'.split())
+
+
+def _MoveSpecialChunksToFront(data):
+  '''Move special chunks immediately after the IHDR chunk (so that the PNG
+  remains valid). Also delete ancillary chunks that are not on our whitelist.
+  '''
+  first = [_PNG_MAGIC]
+  special_chunks = []
+  rest = []
+  for chunk in _ChunkifyPNG(data):
+    type = chunk[4:8]
+    critical = type < b'a'
+    if type == _FIRST_CHUNK:
+      first.append(chunk)
+    elif type in _SPECIAL_CHUNKS:
+      special_chunks.append(chunk)
+    elif critical or type in _ANCILLARY_CHUNKS_TO_LEAVE:
+      rest.append(chunk)
+  return b''.join(first + special_chunks + rest)
+
+
+def _ChunkifyPNG(data):
+  '''Given a PNG image, yield its chunks in order.'''
+  assert data.startswith(_PNG_MAGIC)
+  pos = 8
+  while pos != len(data):
+    length = 12 + struct.unpack_from('>I', data, pos)[0]
+    assert 12 <= length <= len(data) - pos
+    yield data[pos:pos+length]
+    pos += length
+
+
+def _MakeBraceGlob(strings):
+  '''Given ['foo', 'bar'], return '{foo,bar}', for error reporting.
+  '''
+  if len(strings) == 1:
+    return strings[0]
+  else:
+    return '{' + ','.join(strings) + '}'
+
+
+class ChromeScaledImage(interface.GathererBase):
+  '''Represents an image that exists in multiple layout variants
+  (e.g. "default", "touch") and multiple scale variants
+  (e.g. "100_percent", "200_percent").
+  '''
+
+  split_context_re_ = lazy_re.compile(r'(.+)_(\d+)_percent\Z')
+
+  def _FindInputFile(self):
+    output_context = self.grd_node.GetRoot().output_context
+    match = self.split_context_re_.match(output_context)
+    if not match:
+      raise exception.MissingMandatoryAttribute(
+          'All <output> nodes must have an appropriate context attribute'
+          ' (e.g. context="touch_200_percent")')
+    req_layout, req_scale = match.group(1), int(match.group(2))
+
+    layouts = [req_layout]
+    try_default_layout = self.grd_node.GetRoot().fallback_to_default_layout
+    if try_default_layout and 'default' not in layouts:
+      layouts.append('default')
+
+    scales = [req_scale]
+    try_low_res = self.grd_node.FindBooleanAttribute(
+        'fallback_to_low_resolution', default=False, skip_self=False)
+    if try_low_res and 100 not in scales:
+      scales.append(100)
+
+    for layout in layouts:
+      for scale in scales:
+        dir = '%s_%s_percent' % (layout, scale)
+        path = os.path.join(dir, self.rc_file)
+        if os.path.exists(self.grd_node.ToRealPath(path)):
+          return path, scale, req_scale
+
+    if not try_default_layout:
+      # The file was not found in the specified output context and it was
+      # explicitly indicated that the default context should not be searched
+      # as a fallback, so return an empty path.
+      return None, 100, req_scale
+
+    # The file was found in neither the specified context nor the default
+    # context, so raise an exception.
+    dir = "%s_%s_percent" % (_MakeBraceGlob(layouts),
+                             _MakeBraceGlob([str(x) for x in scales]))
+    raise exception.FileNotFound(
+        'Tried ' + self.grd_node.ToRealPath(os.path.join(dir, self.rc_file)))
+
+  def GetInputPath(self):
+    path, scale, req_scale = self._FindInputFile()
+    return path
+
+  def Parse(self):
+    pass
+
+  def GetTextualIds(self):
+    return [self.extkey]
+
+  def GetData(self, lang, encoding):
+    assert encoding == util.BINARY
+
+    path, scale, req_scale = self._FindInputFile()
+    if path is None:
+      return None
+
+    data = util.ReadFile(self.grd_node.ToRealPath(path), util.BINARY)
+    data = _RescaleImage(data, scale, req_scale)
+    data = _MoveSpecialChunksToFront(data)
+    return data
+
+  def Translate(self, *args, **kwargs):
+    return self.GetData()
diff --git a/tools/grit/grit/gather/chrome_scaled_image_unittest.py b/tools/grit/grit/gather/chrome_scaled_image_unittest.py
new file mode 100644
index 0000000000..1cebfc6de2
--- /dev/null
+++ b/tools/grit/grit/gather/chrome_scaled_image_unittest.py
@@ -0,0 +1,209 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for ChromeScaledImage.'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                               '../..')))
+
+import re
+import struct
+import unittest
+import zlib
+
+from grit import exception
+from grit import util
+from grit.format import data_pack
+from grit.tool import build
+
+
+_OUTFILETYPES = [
+  ('.h', 'rc_header'),
+  ('_map.cc', 'resource_map_source'),
+  ('_map.h', 'resource_map_header'),
+  ('.pak', 'data_package'),
+  ('.rc', 'rc_all'),
+]
+
+
+_PNG_HEADER = (
+    b'\x89\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52'
+    b'\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90\x77\x53'
+    b'\xde')
+_PNG_FOOTER = (
+    b'\x00\x00\x00\x0c\x49\x44\x41\x54\x18\x57\x63\xf8\xff\xff\x3f\x00'
+    b'\x05\xfe\x02\xfe\xa7\x35\x81\x84\x00\x00\x00\x00\x49\x45\x4e\x44'
+    b'\xae\x42\x60\x82')
+
+
+def _MakePNG(chunks):
+  # Python 3 changed the return value of zlib.crc32 to an unsigned int.
+  format = 'i' if sys.version_info.major < 3 else 'I'
+  pack_int32 = struct.Struct('>' + format).pack
+  chunks = [pack_int32(len(payload)) + type + payload +
+            pack_int32(zlib.crc32(type + payload))
+            for type, payload in chunks]
+  return _PNG_HEADER + b''.join(chunks) + _PNG_FOOTER
+
+
+def _GetFilesInPak(pakname):
+  '''Get a set of the files that were actually included in the .pak output.
+  '''
+  return set(data_pack.ReadDataPack(pakname).resources.values())
+
+
+def _GetFilesInRc(rcname, tmp_dir, contents):
+  '''Get a set of the files that were actually included in the .rc output.
+  '''
+  data = util.ReadFile(rcname, util.BINARY).decode('utf-16')
+  contents = dict((tmp_dir.GetPath(k), v) for k, v in contents.items())
+  return set(contents[os.path.normpath(m.group(1))]
+             for m in re.finditer(r'(?m)^\w+\s+BINDATA\s+"([^"]+)"$', data))
+
+
+def _MakeFallbackAttr(fallback):
+  if fallback is None:
+    return ''
+  else:
+    return ' fallback_to_low_resolution="%s"' % ('false', 'true')[fallback]
+
+
+def _Structures(fallback, *body):
+  return '<structures%s>\n%s\n</structures>' % (
+      _MakeFallbackAttr(fallback), '\n'.join(body))
+
+
+def _Structure(name, file, fallback=None):
+  return '<structure name="%s" file="%s" type="chrome_scaled_image"%s />' % (
+      name, file, _MakeFallbackAttr(fallback))
+
+
+def _If(expr, *body):
+  return '<if expr="%s">\n%s\n</if>' % (expr, '\n'.join(body))
+
+
+def _RunBuildTest(self, structures, inputs, expected_outputs, skip_rc=False,
+                  layout_fallback=''):
+  outputs = '\n'.join('<output filename="out/%s%s" type="%s" context="%s"%s />'
+                              % (context, ext, type, context, layout_fallback)
+                      for ext, type in _OUTFILETYPES
+                      for context in expected_outputs)
+
+  infiles = {
+      'in/in.grd': ('''<?xml version="1.0" encoding="UTF-8"?>
+      <grit latest_public_release="0" current_release="1">
+        <outputs>
+          %s
+        </outputs>
+        <release seq="1">
+          %s
+        </release>
+      </grit>
+      ''' % (outputs, structures)).encode('utf-8'),
+  }
+  for pngpath, pngdata in inputs.items():
+    normpath = os.path.normpath('in/' + pngpath)
+    infiles[normpath] = pngdata
+  class Options(object):
+    pass
+
+  with util.TempDir(infiles, mode='wb') as tmp_dir:
+    with tmp_dir.AsCurrentDir():
+      options = Options()
+      options.input = tmp_dir.GetPath('in/in.grd')
+      options.verbose = False
+      options.extra_verbose = False
+      build.RcBuilder().Run(options, [])
+    for context, expected_data in expected_outputs.items():
+      self.assertEquals(expected_data,
+                        _GetFilesInPak(tmp_dir.GetPath('out/%s.pak' % context)))
+      if not skip_rc:
+        self.assertEquals(expected_data,
+                          _GetFilesInRc(tmp_dir.GetPath('out/%s.rc' % context),
+                                        tmp_dir, infiles))
+
+
+class ChromeScaledImageUnittest(unittest.TestCase):
+  def testNormalFallback(self):
+    d123a = _MakePNG([(b'AbCd', b'')])
+    t123a = _MakePNG([(b'EfGh', b'')])
+    d123b = _MakePNG([(b'IjKl', b'')])
+    _RunBuildTest(self,
+        _Structures(None,
+            _Structure('IDR_A', 'a.png'),
+            _Structure('IDR_B', 'b.png'),
+        ),
+        {'default_123_percent/a.png': d123a,
+         'tactile_123_percent/a.png': t123a,
+         'default_123_percent/b.png': d123b,
+        },
+        {'default_123_percent': set([d123a, d123b]),
+         'tactile_123_percent': set([t123a, d123b]),
+        })
+
+  def testNormalFallbackFailure(self):
+    self.assertRaises(
+        exception.FileNotFound, _RunBuildTest, self,
+        _Structures(
+            None,
+            _Structure('IDR_A', 'a.png'),
+        ), {
+            'default_100_percent/a.png': _MakePNG([(b'AbCd', b'')]),
+            'tactile_100_percent/a.png': _MakePNG([(b'EfGh', b'')]),
+        }, {'tactile_123_percent': 'should fail before using this'})
+
+  def testLowresFallback(self):
+    png = _MakePNG([(b'Abcd', b'')])
+    png_with_csCl = _MakePNG([(b'csCl', b''), (b'Abcd', b'')])
+    for outer in (None, False, True):
+      for inner in (None, False, True):
+        args = (
+            self,
+            _Structures(outer,
+                _Structure('IDR_A', 'a.png', inner),
+            ),
+            {'default_100_percent/a.png': png},
+            {'tactile_200_percent': set([png_with_csCl])})
+        if inner or (inner is None and outer):
+          # should fall back to 100%
+          _RunBuildTest(*args, skip_rc=True)
+        else:
+          # shouldn't fall back
+          self.assertRaises(exception.FileNotFound, _RunBuildTest, *args)
+
+    # Test fallback failure with fallback_to_low_resolution=True
+    self.assertRaises(exception.FileNotFound,
+        _RunBuildTest, self,
+            _Structures(True,
+                _Structure('IDR_A', 'a.png'),
+            ),
+            {},  # no files
+            {'tactile_123_percent': 'should fail before using this'})
+
+  def testNoFallbackToDefaultLayout(self):
+    d123a = _MakePNG([(b'AbCd', b'')])
+    t123a = _MakePNG([(b'EfGh', b'')])
+    d123b = _MakePNG([(b'IjKl', b'')])
+    _RunBuildTest(self,
+        _Structures(None,
+            _Structure('IDR_A', 'a.png'),
+            _Structure('IDR_B', 'b.png'),
+        ),
+        {'default_123_percent/a.png': d123a,
+         'tactile_123_percent/a.png': t123a,
+         'default_123_percent/b.png': d123b,
+        },
+        {'default_123_percent': set([d123a, d123b]),
+         'tactile_123_percent': set([t123a]),
+        },
+        layout_fallback=' fallback_to_default_layout="false"')
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/gather/interface.py b/tools/grit/grit/gather/interface.py
new file mode 100644
index 0000000000..15d64f9326
--- /dev/null
+++ b/tools/grit/grit/gather/interface.py
@@ -0,0 +1,172 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Interface for all gatherers.
+'''
+
+from __future__ import print_function
+
+import os.path
+
+import six
+
+from grit import clique
+from grit import util
+
+
+class GathererBase(object):
+  '''Interface for all gatherer implementations.  Subclasses must implement
+  all methods that raise NotImplemented.'''
+
+  def __init__(self, rc_file, extkey=None, encoding='cp1252', is_skeleton=False):
+    '''Initializes the gatherer object's attributes, but does not attempt to
+    read the input file.
+
+    Args:
+      rc_file: The 'file' attribute of the <structure> node (usually the
+               relative path to the source file).
+      extkey: e.g. 'ID_MY_DIALOG'
+      encoding: e.g. 'utf-8'
+      is_skeleton: Indicates whether this gatherer is a skeleton gatherer, in
+                   which case we should not do some types of processing on the
+                   translateable bits.
+    '''
+    self.rc_file = rc_file
+    self.extkey = extkey
+    self.encoding = encoding
+    # A default uberclique that is local to this object.  Users can override
+    # this with the uberclique they are using.
+    self.uberclique = clique.UberClique()
+    # Indicates whether this gatherer is a skeleton gatherer, in which case
+    # we should not do some types of processing on the translateable bits.
+    self.is_skeleton = is_skeleton
+    # Stores the grd node on which this gatherer is running. This allows
+    # evaluating expressions.
+    self.grd_node = None
+
+  def SetAttributes(self, attrs):
+    '''Sets node attributes used by the gatherer.
+
+    By default, this does nothing.  If special handling is desired, it should be
+    overridden by the child gatherer.
+
+    Args:
+      attrs: The mapping of node attributes.
+    '''
+    pass
+
+  def SetDefines(self, defines):
+    '''Sets global defines used by the gatherer.
+
+    By default, this does nothing.  If special handling is desired, it should be
+    overridden by the child gatherer.
+
+    Args:
+      defines: The mapping of define values.
+    '''
+    pass
+
+  def SetGrdNode(self, node):
+    '''Sets the grd node on which this gatherer is running.
+    '''
+    self.grd_node = node
+
+  def SetUberClique(self, uberclique):
+    '''Overrides the default uberclique so that cliques created by this object
+    become part of the uberclique supplied by the user.
+    '''
+    self.uberclique = uberclique
+
+  def Parse(self):
+    '''Reads and parses the contents of what is being gathered.'''
+    raise NotImplementedError()
+
+  def GetData(self, lang, encoding):
+    '''Returns the data to be added to the DataPack for this node or None if
+    this node does not add a DataPack entry.
+    '''
+    return None
+
+  def GetText(self):
+    '''Returns the text of what is being gathered.'''
+    raise NotImplementedError()
+
+  def GetTextualIds(self):
+    '''Returns the mnemonic IDs that need to be defined for the resource
+    being gathered to compile correctly.'''
+    return []
+
+  def GetCliques(self):
+    '''Returns the MessageClique objects for all translateable portions.'''
+    return []
+
+  def GetInputPath(self):
+    return self.rc_file
+
+  def GetHtmlResourceFilenames(self):
+    """Returns a set of all filenames inlined by this gatherer."""
+    return []
+
+  def Translate(self, lang, pseudo_if_not_available=True,
+                skeleton_gatherer=None, fallback_to_english=False):
+    '''Returns the resource being gathered, with translateable portions filled
+    with the translation for language 'lang'.
+
+    If pseudo_if_not_available is true, a pseudotranslation will be used for any
+    message that doesn't have a real translation available.
+
+    If no translation is available and pseudo_if_not_available is false,
+    fallback_to_english controls the behavior.  If it is false, throw an error.
+    If it is true, use the English version of the message as its own
+    "translation".
+
+    If skeleton_gatherer is specified, the translation will use the nontranslateable
+    parts from the gatherer 'skeleton_gatherer', which must be of the same type
+    as 'self'.
+
+    If fallback_to_english
+
+    Args:
+      lang: 'en'
+      pseudo_if_not_available: True | False
+      skeleton_gatherer: other_gatherer
+      fallback_to_english: True | False
+
+    Return:
+      e.g. 'ID_THIS_SECTION TYPE\n...BEGIN\n  "Translated message"\n......\nEND'
+
+    Raises:
+      grit.exception.NotReady() if used before Parse() has been successfully
+      called.
+      grit.exception.NoSuchTranslation() if 'pseudo_if_not_available' and
+      fallback_to_english are both false and there is no translation for the
+      requested language.
+    '''
+    raise NotImplementedError()
+
+  def SubstituteMessages(self, substituter):
+    '''Applies substitutions to all messages in the gatherer.
+
+    Args:
+      substituter: a grit.util.Substituter object.
+    '''
+    pass
+
+  def SetFilenameExpansionFunction(self, fn):
+    '''Sets a function for rewriting filenames before gathering.'''
+    pass
+
+  # TODO(benrg): Move this elsewhere, since it isn't part of the interface.
+  def _LoadInputFile(self):
+    '''A convenience function for subclasses that loads the contents of the
+    input file.
+    '''
+    if isinstance(self.rc_file, six.string_types):
+      path = self.GetInputPath()
+      # Hack: some unit tests supply an absolute path and no root node.
+      if not os.path.isabs(path):
+        path = self.grd_node.ToRealPath(path)
+      return util.ReadFile(path, self.encoding)
+    else:
+      return self.rc_file.read()
diff --git a/tools/grit/grit/gather/json_loader.py b/tools/grit/grit/gather/json_loader.py
new file mode 100644
index 0000000000..058e5f17ae
--- /dev/null
+++ b/tools/grit/grit/gather/json_loader.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from __future__ import print_function
+
+from grit.gather import interface
+
+
+class JsonLoader(interface.GathererBase):
+  '''A simple gatherer that loads and parses a JSON file.'''
+
+  def Parse(self):
+    '''Reads and parses the text of self._json_text into the data structure in
+    self._data.
+    '''
+    self._json_text = self._LoadInputFile()
+    self._data = None
+
+    globs = {}
+    exec('data = ' + self._json_text, globs)
+    self._data = globs['data']
+
+  def GetData(self, lang, encoding):
+    '''Returns the parsed JSON data.'''
+    assert encoding == 'utf-8'
+    return self._data
diff --git a/tools/grit/grit/gather/policy_json.py b/tools/grit/grit/gather/policy_json.py
new file mode 100644
index 0000000000..6621c5f3c4
--- /dev/null
+++ b/tools/grit/grit/gather/policy_json.py
@@ -0,0 +1,325 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Support for "policy_templates.json" format used by the policy template
+generator as a source for generating ADM,ADMX,etc files.'''
+
+from __future__ import print_function
+
+import json
+import sys
+
+import six
+
+from grit.gather import skeleton_gatherer
+from grit import util
+from grit import tclib
+from xml.dom import minidom
+from xml.parsers.expat import ExpatError
+
+
+class PolicyJson(skeleton_gatherer.SkeletonGatherer):
+  '''Collects and translates the following strings from policy_templates.json:
+    - captions, descriptions, labels and Android app support details of policies
+    - captions of enumeration items
+    - misc strings from the 'messages' section
+     Translatable strings may have untranslateable placeholders with the same
+     format that is used in .grd files.
+  '''
+
+  def _AddEndline(self, add_comma):
+    '''Adds an endline to the skeleton tree. If add_comma is true, adds a
+       comma before the endline.
+
+    Args:
+      add_comma: A boolean to add a comma or not.
+    '''
+    self._AddNontranslateableChunk(',\n' if add_comma else '\n')
+
+  def _ParsePlaceholder(self, placeholder, msg):
+    '''Extracts a placeholder from a DOM node and adds it to a tclib Message.
+
+    Args:
+      placeholder: A DOM node of the form:
+        <ph name="PLACEHOLDER_NAME">Placeholder text<ex>Example value</ex></ph>
+      msg: The placeholder is added to this message.
+    '''
+    text = []
+    example_text = []
+    for node1 in placeholder.childNodes:
+      if (node1.nodeType == minidom.Node.TEXT_NODE):
+        text.append(node1.data)
+      elif (node1.nodeType == minidom.Node.ELEMENT_NODE and
+            node1.tagName == 'ex'):
+        for node2 in node1.childNodes:
+          example_text.append(node2.toxml())
+      else:
+        raise Exception('Unexpected element inside a placeholder: ' +
+                        node2.toxml())
+    if example_text == []:
+      # In such cases the original text is okay for an example.
+      example_text = text
+
+    replaced_text = self.Escape(''.join(text).strip())
+    replaced_text = replaced_text.replace('$1', self._config['app_name'])
+    replaced_text = replaced_text.replace('$2', self._config['os_name'])
+    replaced_text = replaced_text.replace('$3', self._config['frame_name'])
+
+    msg.AppendPlaceholder(tclib.Placeholder(
+        placeholder.attributes['name'].value,
+        replaced_text,
+        ''.join(example_text).strip()))
+
+  def _ParseMessage(self, string, desc):
+    '''Parses a given string and adds it to the output as a translatable chunk
+    with a given description.
+
+    Args:
+      string: The message string to parse.
+      desc: The description of the message (for the translators).
+    '''
+    msg = tclib.Message(description=desc)
+    xml = '<msg>' + string + '</msg>'
+    try:
+      node = minidom.parseString(xml).childNodes[0]
+    except ExpatError:
+      reason = '''Input isn't valid XML (has < & > been escaped?): ''' + string
+      six.reraise(Exception, reason, sys.exc_info()[2])
+
+    for child in node.childNodes:
+      if child.nodeType == minidom.Node.TEXT_NODE:
+        msg.AppendText(child.data)
+      elif child.nodeType == minidom.Node.ELEMENT_NODE:
+        if child.tagName == 'ph':
+          self._ParsePlaceholder(child, msg)
+        else:
+          raise Exception("Not implemented.")
+      else:
+        raise Exception("Not implemented.")
+    self.skeleton_.append(self.uberclique.MakeClique(msg))
+
+  def _ParseNode(self, node):
+    '''Traverses the subtree of a DOM node, and register a tclib message for
+    all the <message> nodes.
+    '''
+    att_text = []
+    if node.attributes:
+      for key, value in sorted(node.attributes.items()):
+        att_text.append(' %s=\"%s\"' % (key, value))
+    self._AddNontranslateableChunk("<%s%s>" %
+                                   (node.tagName, ''.join(att_text)))
+    if node.tagName == 'message':
+      msg = tclib.Message(description=node.attributes['desc'])
+      for child in node.childNodes:
+        if child.nodeType == minidom.Node.TEXT_NODE:
+          if msg == None:
+            self._AddNontranslateableChunk(child.data)
+          else:
+            msg.AppendText(child.data)
+        elif child.nodeType == minidom.Node.ELEMENT_NODE:
+          if child.tagName == 'ph':
+            self._ParsePlaceholder(child, msg)
+        else:
+          assert False
+      self.skeleton_.append(self.uberclique.MakeClique(msg))
+    else:
+      for child in node.childNodes:
+        if child.nodeType == minidom.Node.TEXT_NODE:
+          self._AddNontranslateableChunk(child.data)
+        elif node.nodeType == minidom.Node.ELEMENT_NODE:
+          self._ParseNode(child)
+
+    self._AddNontranslateableChunk("</%s>" % node.tagName)
+
+  def _AddIndentedNontranslateableChunk(self, depth, string):
+    '''Adds a nontranslateable chunk of text to the internally stored output.
+
+    Args:
+      depth: The number of double spaces to prepend to the next argument string.
+      string: The chunk of text to add.
+    '''
+    result = []
+    while depth > 0:
+      result.append('  ')
+      depth = depth - 1
+    result.append(string)
+    self._AddNontranslateableChunk(''.join(result))
+
+  def _GetDescription(self, item, item_type, parent_item, key):
+    '''Creates a description for a translatable message. The description gives
+    some context for the person who will translate this message.
+
+    Args:
+      item: A policy or an enumeration item.
+      item_type: 'enum_item' | 'policy'
+      parent_item: The owner of item. (A policy of type group or enum.)
+      key: The name of the key to parse.
+      depth: The level of indentation.
+    '''
+    key_map = {
+      'desc': 'Description',
+      'caption': 'Caption',
+      'label': 'Label',
+      'arc_support': 'Information about the effect on Android apps'
+    }
+    if item_type == 'policy':
+      return ('%s of the policy named %s [owner(s): %s]' %
+              (key_map[key], item['name'],
+               ','.join(item['owners'] if 'owners' in item else 'unknown')))
+    if item_type == 'enum_item':
+      return ('%s of the option named %s in policy %s [owner(s): %s]' %
+              (key_map[key], item['name'], parent_item['name'],
+               ','.join(parent_item['owners'] if 'owners' in parent_item else 'unknown')))
+    raise Exception('Unexpected type %s' % item_type)
+
+  def _AddSchemaKeys(self, obj, depth):
+    obj_type = type(obj)
+    if obj_type == dict:
+      self._AddNontranslateableChunk('{\n')
+      keys = sorted(obj.keys())
+      for count, (key) in enumerate(keys, 1):
+        json_key = "%s: " % json.dumps(key)
+        self._AddIndentedNontranslateableChunk(depth + 1, json_key)
+        if key == 'description' and type(obj[key]) == str:
+          self._AddNontranslateableChunk("\"")
+          self._ParseMessage(obj[key], 'Description of schema property')
+          self._AddNontranslateableChunk("\"")
+        elif type(obj[key]) in (bool, int, str):
+          self._AddSchemaKeys(obj[key], 0)
+        else:
+          self._AddSchemaKeys(obj[key], depth + 1)
+        self._AddEndline(count < len(keys))
+      self._AddIndentedNontranslateableChunk(depth, '}')
+    elif obj_type == list:
+      self._AddNontranslateableChunk('[\n')
+      for count, (item) in enumerate(obj, 1):
+        self._AddSchemaKeys(item, depth + 1)
+        self._AddEndline(count < len(obj))
+      self._AddIndentedNontranslateableChunk(depth, ']')
+    elif obj_type in (bool, int, str):
+      self._AddIndentedNontranslateableChunk(depth, json.dumps(obj))
+    else:
+      raise Exception('Invalid schema object: %s' % obj)
+
+  def _AddPolicyKey(self, item, item_type, parent_item, key, depth):
+    '''Given a policy/enumeration item and a key, adds that key and its value
+    into the output.
+    E.g.:
+       'example_value': 123
+    If key indicates that the value is a translatable string, then it is parsed
+    as a translatable string.
+
+    Args:
+      item: A policy or an enumeration item.
+      item_type: 'enum_item' | 'policy'
+      parent_item: The owner of item. (A policy of type group or enum.)
+      key: The name of the key to parse.
+      depth: The level of indentation.
+    '''
+    self._AddIndentedNontranslateableChunk(depth, "%s: " % json.dumps(key))
+    if key in ('desc', 'caption', 'label', 'arc_support'):
+      self._AddNontranslateableChunk("\"")
+      self._ParseMessage(
+          item[key],
+          self._GetDescription(item, item_type, parent_item, key))
+      self._AddNontranslateableChunk("\"")
+    elif key in ('schema', 'validation_schema', 'description_schema'):
+      self._AddSchemaKeys(item[key], depth)
+    else:
+      self._AddNontranslateableChunk(json.dumps(item[key], ensure_ascii=False))
+
+  def _AddItems(self, items, item_type, parent_item, depth):
+    '''Parses and adds a list of items from the JSON file. Items can be policies
+    or parts of an enum policy.
+
+    Args:
+      items: Either a list of policies or a list of dictionaries.
+      item_type: 'enum_item' | 'policy'
+      parent_item: If items contains a list of policies, then this is the policy
+        group that owns them. If items contains a list of enumeration items,
+        then this is the enum policy that holds them.
+      depth: Indicates the depth of our position in the JSON hierarchy. Used to
+        add nice line-indent to the output.
+    '''
+    for item_count, (item1) in enumerate(items, 1):
+      self._AddIndentedNontranslateableChunk(depth, "{\n")
+      keys = sorted(item1.keys())
+      for keys_count, (key) in enumerate(keys, 1):
+        if key == 'items':
+          self._AddIndentedNontranslateableChunk(depth + 1, "\"items\": [\n")
+          self._AddItems(item1['items'], 'enum_item', item1, depth + 2)
+          self._AddIndentedNontranslateableChunk(depth + 1, "]")
+        elif key == 'policies' and all(not isinstance(x, str)
+                                       for x in item1['policies']):
+          self._AddIndentedNontranslateableChunk(depth + 1, "\"policies\": [\n")
+          self._AddItems(item1['policies'], 'policy', item1, depth + 2)
+          self._AddIndentedNontranslateableChunk(depth + 1, "]")
+        else:
+          self._AddPolicyKey(item1, item_type, parent_item, key, depth + 1)
+        self._AddEndline(keys_count < len(keys))
+      self._AddIndentedNontranslateableChunk(depth, "}")
+      self._AddEndline(item_count < len(items))
+
+  def _AddMessages(self):
+    '''Processed and adds the 'messages' section to the output.'''
+    self._AddNontranslateableChunk("  \"messages\": {\n")
+    messages = self.data['messages'].items()
+    for count, (name, message) in enumerate(messages, 1):
+      self._AddNontranslateableChunk("    %s: {\n" % json.dumps(name))
+      self._AddNontranslateableChunk("      \"text\": \"")
+      self._ParseMessage(message['text'], message['desc'])
+      self._AddNontranslateableChunk("\"\n")
+      self._AddNontranslateableChunk("    }")
+      self._AddEndline(count < len(self.data['messages']))
+    self._AddNontranslateableChunk("  }\n")
+
+  # Although we use the RegexpGatherer base class, we do not use the
+  # _RegExpParse method of that class to implement Parse().  Instead, we
+  # parse using a DOM parser.
+  def Parse(self):
+    if self.have_parsed_:
+      return
+    self.have_parsed_ = True
+
+    self.text_ = self._LoadInputFile()
+    if util.IsExtraVerbose():
+      print(self.text_)
+
+    self.data = eval(self.text_)
+
+    self._AddNontranslateableChunk('{\n')
+    self._AddNontranslateableChunk("  \"policy_definitions\": [\n")
+    self._AddItems(self.data['policy_definitions'], 'policy', None, 2)
+    self._AddNontranslateableChunk("  ],\n")
+    self._AddNontranslateableChunk("  \"policy_atomic_group_definitions\": [\n")
+    if 'policy_atomic_group_definitions' in self.data:
+      self._AddItems(self.data['policy_atomic_group_definitions'],
+                    'policy', None, 2)
+    self._AddNontranslateableChunk("  ],\n")
+    self._AddMessages()
+    self._AddNontranslateableChunk('\n}')
+
+  def Escape(self, text):
+    return json.dumps(text, ensure_ascii=False)[1:-1]
+
+  def SetDefines(self, defines):
+    if not defines:
+      raise Exception('Must pass valid defines')
+
+    if '_chromium' in defines:
+      self._config = {
+        'build': 'chromium',
+        'app_name': 'Chromium',
+        'frame_name': 'Chromium Frame',
+        'os_name': 'Chromium OS',
+      }
+    elif '_google_chrome' in defines:
+      self._config = {
+        'build': 'chrome',
+        'app_name': 'Google Chrome',
+        'frame_name': 'Google Chrome Frame',
+        'os_name': 'Google Chrome OS',
+      }
+    else:
+      raise Exception('Unknown build')
diff --git a/tools/grit/grit/gather/policy_json_unittest.py b/tools/grit/grit/gather/policy_json_unittest.py
new file mode 100644
index 0000000000..214cd276aa
--- /dev/null
+++ b/tools/grit/grit/gather/policy_json_unittest.py
@@ -0,0 +1,347 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.policy_json'''
+
+from __future__ import print_function
+
+import json
+import os
+import re
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import policy_json
+
+class PolicyJsonUnittest(unittest.TestCase):
+
+  def GetExpectedOutput(self, original):
+    expected = eval(original)
+    for key, message in expected['messages'].items():
+      del message['desc']
+    return expected
+
+  def testEmpty(self):
+    original = """{
+      'policy_definitions': [],
+      'policy_atomic_group_definitions': [],
+      'messages': {}
+      }"""
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 0)
+    self.failUnless(eval(original) == json.loads(gatherer.Translate('en')))
+
+  def testGeneralPolicy(self):
+    original = (
+        "{"
+        "  'policy_definitions': ["
+        "    {"
+        "      'name': 'HomepageLocation',"
+        "      'type': 'string',"
+        "      'owners': ['foo@bar.com'],"
+        "      'supported_on': ['chrome.*:8-'],"
+        "      'features': {'dynamic_refresh': 1},"
+        "      'example_value': 'http://chromium.org',"
+        "      'caption': 'nothing special 1',"
+        "      'desc': 'nothing special 2',"
+        "      'label': 'nothing special 3',"
+        "    },"
+        "  ],"
+        "  'policy_atomic_group_definitions': [],"
+        "  'messages': {"
+        "    'msg_identifier': {"
+        "      'text': 'nothing special 3',"
+        "      'desc': 'nothing special descr 3',"
+        "    }"
+        "  }"
+        "}")
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 4)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  def testEnum(self):
+    original = (
+        "{"
+        "  'policy_definitions': ["
+        "    {"
+        "      'name': 'Policy1',"
+        "      'owners': ['a@b'],"
+        "      'items': ["
+        "        {"
+        "          'name': 'Item1',"
+        "          'caption': 'nothing special',"
+        "        }"
+        "      ]"
+        "    },"
+        "  ],"
+        "  'policy_atomic_group_definitions': [],"
+        "  'messages': {}"
+        "}")
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  def testSchema(self):
+    original = ("{"
+                "  'policy_definitions': ["
+                "    {"
+                "      'name': 'Policy1',"
+                "      'schema': {"
+                "        'type': 'object',"
+                "        'properties': {"
+                "          'outer': {"
+                "            'description': 'outer description',"
+                "            'type': 'object',"
+                "            'inner': {"
+                "              'description': 'inner description',"
+                "              'type': 'integer', 'minimum': 0, 'maximum': 100"
+                "            },"
+                "            'inner2': {"
+                "              'description': 'inner2 description',"
+                "              'type': 'integer',"
+                "              'enum': [ 1, 2, 3 ],"
+                "              'sensitiveValue': True"
+                "            },"
+                "          },"
+                "        },"
+                "      },"
+                "      'caption': 'nothing special',"
+                "      'owners': ['a@b']"
+                "    },"
+                "  ],"
+                "  'policy_atomic_group_definitions': [],"
+                "  'messages': {}"
+                "}")
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 4)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  def testValidationSchema(self):
+    original = ("{"
+                "  'policy_definitions': ["
+                "    {"
+                "      'name': 'Policy1',"
+                "      'owners': ['a@b'],"
+                "      'validation_schema': {"
+                "        'type': 'object',"
+                "        'properties': {"
+                "          'description': 'properties description',"
+                "          'type': 'object',"
+                "        },"
+                "      },"
+                "    },"
+                "  ],"
+                "  'policy_atomic_group_definitions': [],"
+                "  'messages': {}"
+                "}")
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  def testDescriptionSchema(self):
+    original = ("{"
+                "  'policy_definitions': ["
+                "    {"
+                "      'name': 'Policy1',"
+                "      'owners': ['a@b'],"
+                "      'description_schema': {"
+                "        'type': 'object',"
+                "        'properties': {"
+                "          'description': 'properties description',"
+                "          'type': 'object',"
+                "        },"
+                "      },"
+                "    },"
+                "  ],"
+                "  'policy_atomic_group_definitions': [],"
+                "  'messages': {}"
+                "}")
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  # Keeping for backwards compatibility.
+  def testSubPolicyOldFormat(self):
+    original = (
+        "{"
+        "  'policy_definitions': ["
+        "    {"
+        "      'type': 'group',"
+        "      'policies': ["
+        "        {"
+        "          'name': 'Policy1',"
+        "          'caption': 'nothing special',"
+        "          'owners': ['a@b']"
+        "        }"
+        "      ]"
+        "    }"
+        "  ],"
+        "  'policy_atomic_group_definitions': [],"
+        "  'messages': {}"
+        "}")
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  def testSubPolicyNewFormat(self):
+    original = (
+        "{"
+        "  'policy_definitions': ["
+        "    {"
+        "      'type': 'group',"
+        "      'policies': ['Policy1']"
+        "    },"
+        "    {"
+        "      'name': 'Policy1',"
+        "      'caption': 'nothing special',"
+        "      'owners': ['a@b']"
+        "    }"
+        "  ],"
+        "  'policy_atomic_group_definitions': [],"
+        "  'messages': {}"
+        "}")
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  def testEscapingAndLineBreaks(self):
+    original = """{
+        'policy_definitions': [],
+        'policy_atomic_group_definitions': [],
+        'messages': {
+          'msg1': {
+            # The following line will contain two backslash characters when it
+            # ends up in eval().
+            'text': '''backslashes, Sir? \\\\''',
+            'desc': ''
+          },
+          'msg2': {
+            'text': '''quotes, Madam? "''',
+            'desc': ''
+          },
+          'msg3': {
+            # The following line will contain two backslash characters when it
+            # ends up in eval().
+            'text': 'backslashes, Sir? \\\\',
+            'desc': ''
+          },
+          'msg4': {
+            'text': "quotes, Madam? '",
+            'desc': ''
+          },
+          'msg5': {
+            'text': '''what happens
+with a newline?''',
+            'desc': ''
+          },
+          'msg6': {
+            # The following line will contain a backslash+n when it ends up in
+            # eval().
+            'text': 'what happens\\nwith a newline? (Episode 1)',
+            'desc': ''
+          }
+        }
+}"""
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 6)
+    expected = self.GetExpectedOutput(original)
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+
+  def testPlaceholdersChromium(self):
+    original = """{
+        "policy_definitions": [
+          {
+            "name": "Policy1",
+            "caption": "Please install\\n<ph name=\\"PRODUCT_NAME\\">$1<ex>Google Chrome</ex></ph>.",
+            "owners": "a@b"
+          }
+        ],
+        "policy_atomic_group_definitions": [],
+        "messages": {}
+}"""
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.SetDefines({'_chromium': True})
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    expected = json.loads(re.sub('<ph.*ph>', 'Chromium', original))
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+    self.failUnless(gatherer.GetCliques()[0].translateable)
+    msg = gatherer.GetCliques()[0].GetMessage()
+    self.failUnless(len(msg.GetPlaceholders()) == 1)
+    ph = msg.GetPlaceholders()[0]
+    self.failUnless(ph.GetOriginal() == 'Chromium')
+    self.failUnless(ph.GetPresentation() == 'PRODUCT_NAME')
+    self.failUnless(ph.GetExample() == 'Google Chrome')
+
+  def testPlaceholdersChrome(self):
+    original = """{
+        "policy_definitions": [
+          {
+            "name": "Policy1",
+            "caption": "Please install\\n<ph name=\\"PRODUCT_NAME\\">$1<ex>Google Chrome</ex></ph>.",
+            "owners": "a@b"
+          }
+        ],
+        "policy_atomic_group_definitions": [],
+        "messages": {}
+}"""
+    gatherer = policy_json.PolicyJson(StringIO(original))
+    gatherer.SetDefines({'_google_chrome': True})
+    gatherer.Parse()
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    expected = json.loads(re.sub('<ph.*ph>', 'Google Chrome', original))
+    self.failUnless(expected == json.loads(gatherer.Translate('en')))
+    self.failUnless(gatherer.GetCliques()[0].translateable)
+    msg = gatherer.GetCliques()[0].GetMessage()
+    self.failUnless(len(msg.GetPlaceholders()) == 1)
+    ph = msg.GetPlaceholders()[0]
+    self.failUnless(ph.GetOriginal() == 'Google Chrome')
+    self.failUnless(ph.GetPresentation() == 'PRODUCT_NAME')
+    self.failUnless(ph.GetExample() == 'Google Chrome')
+
+  def testGetDescription(self):
+    gatherer = policy_json.PolicyJson({})
+    gatherer.SetDefines({'_google_chrome': True})
+    self.assertEquals(
+        gatherer._GetDescription({'name': 'Policy1', 'owners': ['a@b']},
+                                 'policy', None, 'desc'),
+        'Description of the policy named Policy1 [owner(s): a@b]')
+    self.assertEquals(
+        gatherer._GetDescription({'name': 'Plcy2', 'owners': ['a@b', 'c@d']},
+                                 'policy', None, 'caption'),
+        'Caption of the policy named Plcy2 [owner(s): a@b,c@d]')
+    self.assertEquals(
+        gatherer._GetDescription({'name': 'Plcy3', 'owners': ['a@b']},
+                                 'policy', None, 'label'),
+        'Label of the policy named Plcy3 [owner(s): a@b]')
+    self.assertEquals(
+        gatherer._GetDescription({'name': 'Item'}, 'enum_item',
+                                 {'name': 'Plcy', 'owners': ['a@b']}, 'caption'),
+        'Caption of the option named Item in policy Plcy [owner(s): a@b]')
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/gather/rc.py b/tools/grit/grit/gather/rc.py
new file mode 100644
index 0000000000..dd091d1e18
--- /dev/null
+++ b/tools/grit/grit/gather/rc.py
@@ -0,0 +1,343 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Support for gathering resources from RC files.
+'''
+
+from __future__ import print_function
+
+import re
+
+from grit import exception
+from grit import lazy_re
+from grit import tclib
+
+from grit.gather import regexp
+
+
+# Find portions that need unescaping in resource strings.  We need to be
+# careful that a \\n is matched _first_ as a \\ rather than matching as
+# a \ followed by a \n.
+# TODO(joi) Handle ampersands if we decide to change them into <ph>
+# TODO(joi) May need to handle other control characters than \n
+_NEED_UNESCAPE = lazy_re.compile(r'""|\\\\|\\n|\\t')
+
+# Find portions that need escaping to encode string as a resource string.
+_NEED_ESCAPE = lazy_re.compile(r'"|\n|\t|\\|\ \;')
+
+# How to escape certain characters
+_ESCAPE_CHARS = {
+  '"' : '""',
+  '\n' : '\\n',
+  '\t' : '\\t',
+  '\\' : '\\\\',
+  ' ' : ' '
+}
+
+# How to unescape certain strings
+_UNESCAPE_CHARS = dict([[value, key] for key, value in _ESCAPE_CHARS.items()])
+
+
+
+class Section(regexp.RegexpGatherer):
+  '''A section from a resource file.'''
+
+  @staticmethod
+  def Escape(text):
+    '''Returns a version of 'text' with characters escaped that need to be
+    for inclusion in a resource section.'''
+    def Replace(match):
+      return _ESCAPE_CHARS[match.group()]
+    return _NEED_ESCAPE.sub(Replace, text)
+
+  @staticmethod
+  def UnEscape(text):
+    '''Returns a version of 'text' with escaped characters unescaped.'''
+    def Replace(match):
+      return _UNESCAPE_CHARS[match.group()]
+    return _NEED_UNESCAPE.sub(Replace, text)
+
+  def _RegExpParse(self, rexp, text_to_parse):
+    '''Overrides _RegExpParse to add shortcut group handling.  Otherwise
+    the same.
+    '''
+    super(Section, self)._RegExpParse(rexp, text_to_parse)
+
+    if not self.is_skeleton and len(self.GetTextualIds()) > 0:
+      group_name = self.GetTextualIds()[0]
+      for c in self.GetCliques():
+        c.AddToShortcutGroup(group_name)
+
+  def ReadSection(self):
+    rc_text = self._LoadInputFile()
+
+    out = ''
+    begin_count = 0
+    assert self.extkey
+    first_line_re = re.compile(r'\s*' + self.extkey + r'\b')
+    for line in rc_text.splitlines(True):
+      if out or first_line_re.match(line):
+        out += line
+
+      # we stop once we reach the END for the outermost block.
+      begin_count_was = begin_count
+      if len(out) > 0 and line.strip() == 'BEGIN':
+        begin_count += 1
+      elif len(out) > 0 and line.strip() == 'END':
+        begin_count -= 1
+      if begin_count_was == 1 and begin_count == 0:
+        break
+
+    if len(out) == 0:
+      raise exception.SectionNotFound('%s in file %s' % (self.extkey, self.rc_file))
+
+    self.text_ = out.strip()
+
+
+class Dialog(Section):
+  '''A resource section that contains a dialog resource.'''
+
+  # A typical dialog resource section looks like this:
+  #
+  # IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+  # STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+  # CAPTION "About"
+  # FONT 8, "System", 0, 0, 0x0
+  # BEGIN
+  #     ICON            IDI_KLONK,IDC_MYICON,14,9,20,20
+  #     LTEXT           "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+  #                     SS_NOPREFIX
+  #     LTEXT           "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+  #     DEFPUSHBUTTON   "OK",IDOK,195,6,30,11,WS_GROUP
+  #     CONTROL         "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+  #                     BS_AUTORADIOBUTTON,46,51,84,10
+  # END
+
+  # We are using a sorted set of keys, and we assume that the
+  # group name used for descriptions (type) will come after the "text"
+  # group in alphabetical order. We also assume that there cannot be
+  # more than one description per regular expression match.
+  # If that's not the case some descriptions will be clobbered.
+  dialog_re_ = lazy_re.compile(r'''
+    # The dialog's ID in the first line
+    (?P<id1>[A-Z0-9_]+)\s+DIALOG(EX)?
+    |
+    # The caption of the dialog
+    (?P<type1>CAPTION)\s+"(?P<text1>.*?([^"]|""))"\s
+    |
+    # Lines for controls that have text and an ID
+    \s+(?P<type2>[A-Z]+)\s+"(?P<text2>.*?([^"]|"")?)"\s*,\s*(?P<id2>[A-Z0-9_]+)\s*,
+    |
+    # Lines for controls that have text only
+    \s+(?P<type3>[A-Z]+)\s+"(?P<text3>.*?([^"]|"")?)"\s*,
+    |
+    # Lines for controls that reference other resources
+    \s+[A-Z]+\s+[A-Z0-9_]+\s*,\s*(?P<id3>[A-Z0-9_]*[A-Z][A-Z0-9_]*)
+    |
+    # This matches "NOT SOME_STYLE" so that it gets consumed and doesn't get
+    # matched by the next option (controls that have only an ID and then just
+    # numbers)
+    \s+NOT\s+[A-Z][A-Z0-9_]+
+    |
+    # Lines for controls that have only an ID and then just numbers
+    \s+[A-Z]+\s+(?P<id4>[A-Z0-9_]*[A-Z][A-Z0-9_]*)\s*,
+    ''', re.MULTILINE | re.VERBOSE)
+
+  def Parse(self):
+    '''Knows how to parse dialog resource sections.'''
+    self.ReadSection()
+    self._RegExpParse(self.dialog_re_, self.text_)
+
+
+class Menu(Section):
+  '''A resource section that contains a menu resource.'''
+
+  # A typical menu resource section looks something like this:
+  #
+  # IDC_KLONK MENU
+  # BEGIN
+  #     POPUP "&File"
+  #     BEGIN
+  #         MENUITEM "E&xit",                       IDM_EXIT
+  #         MENUITEM "This be ""Klonk"" me like",   ID_FILE_THISBE
+  #         POPUP "gonk"
+  #         BEGIN
+  #             MENUITEM "Klonk && is ""good""",           ID_GONK_KLONKIS
+  #         END
+  #     END
+  #     POPUP "&Help"
+  #     BEGIN
+  #         MENUITEM "&About ...",                  IDM_ABOUT
+  #     END
+  # END
+
+  # Description used for the messages generated for menus, to explain to
+  # the translators how to handle them.
+  MENU_MESSAGE_DESCRIPTION = (
+    'This message represents a menu. Each of the items appears in sequence '
+    '(some possibly within sub-menus) in the menu. The XX01XX placeholders '
+    'serve to separate items. Each item contains an & (ampersand) character '
+    'in front of the keystroke that should be used as a shortcut for that item '
+    'in the menu. Please make sure that no two items in the same menu share '
+    'the same shortcut.'
+  )
+
+  # A dandy regexp to suck all the IDs and translateables out of a menu
+  # resource
+  menu_re_ = lazy_re.compile(r'''
+    # Match the MENU ID on the first line
+    ^(?P<id1>[A-Z0-9_]+)\s+MENU
+    |
+    # Match the translateable caption for a popup menu
+    POPUP\s+"(?P<text1>.*?([^"]|""))"\s
+    |
+    # Match the caption & ID of a MENUITEM
+    MENUITEM\s+"(?P<text2>.*?([^"]|""))"\s*,\s*(?P<id2>[A-Z0-9_]+)
+    ''', re.MULTILINE | re.VERBOSE)
+
+  def Parse(self):
+    '''Knows how to parse menu resource sections.  Because it is important that
+    menu shortcuts are unique within the menu, we return each menu as a single
+    message with placeholders to break up the different menu items, rather than
+    return a single message per menu item.  we also add an automatic description
+    with instructions for the translators.'''
+    self.ReadSection()
+    self.single_message_ = tclib.Message(description=self.MENU_MESSAGE_DESCRIPTION)
+    self._RegExpParse(self.menu_re_, self.text_)
+
+
+class Version(Section):
+  '''A resource section that contains a VERSIONINFO resource.'''
+
+  # A typical version info resource can look like this:
+  #
+  # VS_VERSION_INFO VERSIONINFO
+  #  FILEVERSION 1,0,0,1
+  #  PRODUCTVERSION 1,0,0,1
+  #  FILEFLAGSMASK 0x3fL
+  # #ifdef _DEBUG
+  #  FILEFLAGS 0x1L
+  # #else
+  #  FILEFLAGS 0x0L
+  # #endif
+  #  FILEOS 0x4L
+  #  FILETYPE 0x2L
+  #  FILESUBTYPE 0x0L
+  # BEGIN
+  #     BLOCK "StringFileInfo"
+  #     BEGIN
+  #         BLOCK "040904e4"
+  #         BEGIN
+  #             VALUE "CompanyName", "TODO: <Company name>"
+  #             VALUE "FileDescription", "TODO: <File description>"
+  #             VALUE "FileVersion", "1.0.0.1"
+  #             VALUE "LegalCopyright", "TODO: (c) <Company name>.  All rights reserved."
+  #             VALUE "InternalName", "res_format_test.dll"
+  #             VALUE "OriginalFilename", "res_format_test.dll"
+  #             VALUE "ProductName", "TODO: <Product name>"
+  #             VALUE "ProductVersion", "1.0.0.1"
+  #         END
+  #     END
+  #     BLOCK "VarFileInfo"
+  #     BEGIN
+  #         VALUE "Translation", 0x409, 1252
+  #     END
+  # END
+  #
+  #
+  # In addition to the above fields, VALUE fields named "Comments" and
+  # "LegalTrademarks" may also be translateable.
+
+  version_re_ = lazy_re.compile(r'''
+    # Match the ID on the first line
+    ^(?P<id1>[A-Z0-9_]+)\s+VERSIONINFO
+    |
+    # Match all potentially translateable VALUE sections
+    \s+VALUE\s+"
+    (
+      CompanyName|FileDescription|LegalCopyright|
+      ProductName|Comments|LegalTrademarks
+    )",\s+"(?P<text1>.*?([^"]|""))"\s
+    ''', re.MULTILINE | re.VERBOSE)
+
+  def Parse(self):
+    '''Knows how to parse VERSIONINFO resource sections.'''
+    self.ReadSection()
+    self._RegExpParse(self.version_re_, self.text_)
+
+  # TODO(joi) May need to override the Translate() method to change the
+  # "Translation" VALUE block to indicate the correct language code.
+
+
+class RCData(Section):
+  '''A resource section that contains some data .'''
+
+  # A typical rcdataresource section looks like this:
+  #
+  # IDR_BLAH        RCDATA      { 1, 2, 3, 4 }
+
+  dialog_re_ = lazy_re.compile(r'''
+    ^(?P<id1>[A-Z0-9_]+)\s+RCDATA\s+(DISCARDABLE)?\s+\{.*?\}
+    ''', re.MULTILINE | re.VERBOSE | re.DOTALL)
+
+  def Parse(self):
+    '''Implementation for resource types w/braces (not BEGIN/END)
+    '''
+    rc_text = self._LoadInputFile()
+
+    out = ''
+    begin_count = 0
+    openbrace_count = 0
+    assert self.extkey
+    first_line_re = re.compile(r'\s*' + self.extkey + r'\b')
+    for line in rc_text.splitlines(True):
+      if out or first_line_re.match(line):
+        out += line
+
+      # We stop once the braces balance (could happen in one line).
+      begin_count_was = begin_count
+      if len(out) > 0:
+        openbrace_count += line.count('{')
+        begin_count += line.count('{')
+        begin_count -= line.count('}')
+      if ((begin_count_was == 1 and begin_count == 0) or
+         (openbrace_count > 0 and begin_count == 0)):
+        break
+
+    if len(out) == 0:
+      raise exception.SectionNotFound('%s in file %s' % (self.extkey, self.rc_file))
+
+    self.text_ = out
+
+    self._RegExpParse(self.dialog_re_, out)
+
+
+class Accelerators(Section):
+  '''An ACCELERATORS table.
+  '''
+
+  # A typical ACCELERATORS section looks like this:
+  #
+  # IDR_ACCELERATOR1 ACCELERATORS
+  # BEGIN
+  #   "^C",           ID_ACCELERATOR32770,    ASCII,  NOINVERT
+  #   "^V",           ID_ACCELERATOR32771,    ASCII,  NOINVERT
+  #   VK_INSERT,      ID_ACCELERATOR32772,    VIRTKEY, CONTROL, NOINVERT
+  # END
+
+  accelerators_re_ = lazy_re.compile(r'''
+    # Match the ID on the first line
+    ^(?P<id1>[A-Z0-9_]+)\s+ACCELERATORS\s+
+    |
+    # Match accelerators specified as VK_XXX
+    \s+VK_[A-Z0-9_]+,\s*(?P<id2>[A-Z0-9_]+)\s*,
+    |
+    # Match accelerators specified as e.g. "^C"
+    \s+"[^"]*",\s+(?P<id3>[A-Z0-9_]+)\s*,
+    ''', re.MULTILINE | re.VERBOSE)
+
+  def Parse(self):
+    '''Knows how to parse ACCELERATORS resource sections.'''
+    self.ReadSection()
+    self._RegExpParse(self.accelerators_re_, self.text_)
diff --git a/tools/grit/grit/gather/rc_unittest.py b/tools/grit/grit/gather/rc_unittest.py
new file mode 100644
index 0000000000..3c26a4342a
--- /dev/null
+++ b/tools/grit/grit/gather/rc_unittest.py
@@ -0,0 +1,372 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.rc'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import rc
+from grit import util
+
+
+class RcUnittest(unittest.TestCase):
+
+  part_we_want = '''IDC_KLONKACC ACCELERATORS
+BEGIN
+    "?",            IDM_ABOUT,              ASCII,  ALT
+    "/",            IDM_ABOUT,              ASCII,  ALT
+END'''
+
+  def testSectionFromFile(self):
+    buf = '''IDC_SOMETHINGELSE BINGO
+BEGIN
+    BLA BLA
+    BLA BLA
+END
+%s
+
+IDC_KLONK BINGOBONGO
+BEGIN
+  HONGO KONGO
+END
+''' % self.part_we_want
+
+    f = StringIO(buf)
+
+    out = rc.Section(f, 'IDC_KLONKACC')
+    out.ReadSection()
+    self.failUnless(out.GetText() == self.part_we_want)
+
+    out = rc.Section(util.PathFromRoot(r'grit/testdata/klonk.rc'),
+                     'IDC_KLONKACC',
+                     encoding='utf-16')
+    out.ReadSection()
+    out_text = out.GetText().replace('\t', '')
+    out_text = out_text.replace(' ', '')
+    self.part_we_want = self.part_we_want.replace(' ', '')
+    self.failUnless(out_text.strip() == self.part_we_want.strip())
+
+
+  def testDialog(self):
+    dlg = rc.Dialog(StringIO('''IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+    ICON            IDI_KLONK,IDC_MYICON,14,9,20,20
+    LTEXT           "klonk Version ""yibbee"" 1.0",IDC_STATIC,49,10,119,8,
+                    SS_NOPREFIX
+    LTEXT           "Copyright (C) 2005",IDC_STATIC,49,20,119,8
+    DEFPUSHBUTTON   "OK",IDOK,195,6,30,11,WS_GROUP
+    CONTROL         "Jack ""Black"" Daniels",IDC_RADIO1,"Button",
+                    BS_AUTORADIOBUTTON,46,51,84,10
+    // try a line where the ID is on the continuation line
+    LTEXT           "blablablabla blablabla blablablablablablablabla blablabla",
+                    ID_SMURF, whatever...
+END
+'''), 'IDD_ABOUTBOX')
+    dlg.Parse()
+    self.failUnless(len(dlg.GetTextualIds()) == 7)
+    self.failUnless(len(dlg.GetCliques()) == 6)
+    self.failUnless(dlg.GetCliques()[1].GetMessage().GetRealContent() ==
+                    'klonk Version "yibbee" 1.0')
+
+    transl = dlg.Translate('en')
+    self.failUnless(transl.strip() == dlg.GetText().strip())
+
+  def testAlternateSkeleton(self):
+    dlg = rc.Dialog(StringIO('''IDD_ABOUTBOX DIALOGEX 22, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "About"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+    LTEXT           "Yipee skippy",IDC_STATIC,49,10,119,8,
+                    SS_NOPREFIX
+END
+'''), 'IDD_ABOUTBOX')
+    dlg.Parse()
+
+    alt_dlg = rc.Dialog(StringIO('''IDD_ABOUTBOX DIALOGEX 040704, 17, 230, 75
+STYLE DS_SETFONT | DS_MODALFRAME | WS_CAPTION | WS_SYSMENU
+CAPTION "XXXXXXXXX"
+FONT 8, "System", 0, 0, 0x0
+BEGIN
+    LTEXT           "XXXXXXXXXXXXXXXXX",IDC_STATIC,110978,10,119,8,
+                    SS_NOPREFIX
+END
+'''), 'IDD_ABOUTBOX')
+    alt_dlg.Parse()
+
+    transl = dlg.Translate('en', skeleton_gatherer=alt_dlg)
+    self.failUnless(transl.count('040704') and
+                    transl.count('110978'))
+    self.failUnless(transl.count('Yipee skippy'))
+
+  def testMenu(self):
+    menu = rc.Menu(StringIO('''IDC_KLONK MENU
+BEGIN
+    POPUP "&File """
+    BEGIN
+        MENUITEM "E&xit",                       IDM_EXIT
+        MENUITEM "This be ""Klonk"" me like",   ID_FILE_THISBE
+        POPUP "gonk"
+        BEGIN
+            MENUITEM "Klonk && is ""good""",           ID_GONK_KLONKIS
+        END
+        MENUITEM "This is a very long menu caption to try to see if we can make the ID go to a continuation line, blablabla blablabla bla blabla blablabla blablabla blablabla blablabla...",
+                                        ID_FILE_THISISAVERYLONGMENUCAPTIONTOTRYTOSEEIFWECANMAKETHEIDGOTOACONTINUATIONLINE
+    END
+    POPUP "&Help"
+    BEGIN
+        MENUITEM "&About ...",                  IDM_ABOUT
+    END
+END'''), 'IDC_KLONK')
+
+    menu.Parse()
+    self.failUnless(len(menu.GetTextualIds()) == 6)
+    self.failUnless(len(menu.GetCliques()) == 1)
+    self.failUnless(len(menu.GetCliques()[0].GetMessage().GetPlaceholders()) ==
+                    9)
+
+    transl = menu.Translate('en')
+    self.failUnless(transl.strip() == menu.GetText().strip())
+
+  def testVersion(self):
+    version = rc.Version(StringIO('''
+VS_VERSION_INFO VERSIONINFO
+ FILEVERSION 1,0,0,1
+ PRODUCTVERSION 1,0,0,1
+ FILEFLAGSMASK 0x3fL
+#ifdef _DEBUG
+ FILEFLAGS 0x1L
+#else
+ FILEFLAGS 0x0L
+#endif
+ FILEOS 0x4L
+ FILETYPE 0x2L
+ FILESUBTYPE 0x0L
+BEGIN
+    BLOCK "StringFileInfo"
+    BEGIN
+        BLOCK "040904e4"
+        BEGIN
+            VALUE "CompanyName", "TODO: <Company name>"
+            VALUE "FileDescription", "TODO: <File description>"
+            VALUE "FileVersion", "1.0.0.1"
+            VALUE "LegalCopyright", "TODO: (c) <Company name>.  All rights reserved."
+            VALUE "InternalName", "res_format_test.dll"
+            VALUE "OriginalFilename", "res_format_test.dll"
+            VALUE "ProductName", "TODO: <Product name>"
+            VALUE "ProductVersion", "1.0.0.1"
+        END
+    END
+    BLOCK "VarFileInfo"
+    BEGIN
+        VALUE "Translation", 0x409, 1252
+    END
+END
+'''.strip()), 'VS_VERSION_INFO')
+    version.Parse()
+    self.failUnless(len(version.GetTextualIds()) == 1)
+    self.failUnless(len(version.GetCliques()) == 4)
+
+    transl = version.Translate('en')
+    self.failUnless(transl.strip() == version.GetText().strip())
+
+
+  def testRegressionDialogBox(self):
+    dialog = rc.Dialog(StringIO('''
+IDD_SIDEBAR_WEATHER_PANEL_PROPPAGE DIALOGEX 0, 0, 205, 157
+STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+    EDITTEXT        IDC_SIDEBAR_WEATHER_NEW_CITY,3,27,112,14,ES_AUTOHSCROLL
+    DEFPUSHBUTTON   "Add Location",IDC_SIDEBAR_WEATHER_ADD,119,27,50,14
+    LISTBOX         IDC_SIDEBAR_WEATHER_CURR_CITIES,3,48,127,89,
+                    LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
+    PUSHBUTTON      "Move Up",IDC_SIDEBAR_WEATHER_MOVE_UP,134,104,50,14
+    PUSHBUTTON      "Move Down",IDC_SIDEBAR_WEATHER_MOVE_DOWN,134,121,50,14
+    PUSHBUTTON      "Remove",IDC_SIDEBAR_WEATHER_DELETE,134,48,50,14
+    LTEXT           "To see current weather conditions and forecasts in the USA, enter the zip code (example: 94043) or city and state (example: Mountain View, CA).",
+                    IDC_STATIC,3,0,199,25
+    CONTROL         "Fahrenheit",IDC_SIDEBAR_WEATHER_FAHRENHEIT,"Button",
+                    BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,3,144,51,10
+    CONTROL         "Celsius",IDC_SIDEBAR_WEATHER_CELSIUS,"Button",
+                    BS_AUTORADIOBUTTON,57,144,38,10
+END'''.strip()), 'IDD_SIDEBAR_WEATHER_PANEL_PROPPAGE')
+    dialog.Parse()
+    self.failUnless(len(dialog.GetTextualIds()) == 10)
+
+
+  def testRegressionDialogBox2(self):
+    dialog = rc.Dialog(StringIO('''
+IDD_SIDEBAR_EMAIL_PANEL_PROPPAGE DIALOG DISCARDABLE 0, 0, 264, 220
+STYLE WS_CHILD
+FONT 8, "MS Shell Dlg"
+BEGIN
+    GROUPBOX        "Email Filters",IDC_STATIC,7,3,250,190
+    LTEXT           "Click Add Filter to create the email filter.",IDC_STATIC,16,41,130,9
+    PUSHBUTTON      "Add Filter...",IDC_SIDEBAR_EMAIL_ADD_FILTER,196,38,50,14
+    PUSHBUTTON      "Remove",IDC_SIDEBAR_EMAIL_REMOVE,196,174,50,14
+    PUSHBUTTON      "", IDC_SIDEBAR_EMAIL_HIDDEN, 200, 178, 5, 5, NOT WS_VISIBLE
+    LISTBOX         IDC_SIDEBAR_EMAIL_LIST,16,60,230,108,
+                    LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP
+    LTEXT           "You can prevent certain emails from showing up in the sidebar with a filter.",
+                    IDC_STATIC,16,18,234,18
+END'''.strip()), 'IDD_SIDEBAR_EMAIL_PANEL_PROPPAGE')
+    dialog.Parse()
+    self.failUnless('IDC_SIDEBAR_EMAIL_HIDDEN' in dialog.GetTextualIds())
+
+
+  def testRegressionMenuId(self):
+    menu = rc.Menu(StringIO('''
+IDR_HYPERMENU_FOLDER MENU
+BEGIN
+    POPUP "HyperFolder"
+    BEGIN
+        MENUITEM "Open Containing Folder",      IDM_OPENFOLDER
+    END
+END'''.strip()), 'IDR_HYPERMENU_FOLDER')
+    menu.Parse()
+    self.failUnless(len(menu.GetTextualIds()) == 2)
+
+  def testRegressionNewlines(self):
+    menu = rc.Menu(StringIO('''
+IDR_HYPERMENU_FOLDER MENU
+BEGIN
+    POPUP "Hyper\\nFolder"
+    BEGIN
+        MENUITEM "Open Containing Folder",      IDM_OPENFOLDER
+    END
+END'''.strip()), 'IDR_HYPERMENU_FOLDER')
+    menu.Parse()
+    transl = menu.Translate('en')
+    # Shouldn't find \\n (the \n shouldn't be changed to \\n)
+    self.failUnless(transl.find('\\\\n') == -1)
+
+  def testRegressionTabs(self):
+    menu = rc.Menu(StringIO('''
+IDR_HYPERMENU_FOLDER MENU
+BEGIN
+    POPUP "Hyper\\tFolder"
+    BEGIN
+        MENUITEM "Open Containing Folder",      IDM_OPENFOLDER
+    END
+END'''.strip()), 'IDR_HYPERMENU_FOLDER')
+    menu.Parse()
+    transl = menu.Translate('en')
+    # Shouldn't find \\t (the \t shouldn't be changed to \\t)
+    self.failUnless(transl.find('\\\\t') == -1)
+
+  def testEscapeUnescape(self):
+    original = 'Hello "bingo"\n How\\are\\you\\n?'
+    escaped = rc.Section.Escape(original)
+    self.failUnless(escaped == 'Hello ""bingo""\\n How\\\\are\\\\you\\\\n?')
+    unescaped = rc.Section.UnEscape(escaped)
+    self.failUnless(unescaped == original)
+
+  def testRegressionPathsWithSlashN(self):
+    original = '..\\\\..\\\\trs\\\\res\\\\nav_first.gif'
+    unescaped = rc.Section.UnEscape(original)
+    self.failUnless(unescaped == '..\\..\\trs\\res\\nav_first.gif')
+
+  def testRegressionDialogItemsTextOnly(self):
+    dialog = rc.Dialog(StringIO('''IDD_OPTIONS_SEARCH DIALOGEX 0, 0, 280, 292
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP |
+    WS_DISABLED | WS_CAPTION | WS_SYSMENU
+CAPTION "Search"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+    GROUPBOX        "Select search buttons and options",-1,7,5,266,262
+    CONTROL         "",IDC_OPTIONS,"SysTreeView32",TVS_DISABLEDRAGDROP |
+                    WS_BORDER | WS_TABSTOP | 0x800,16,19,248,218
+    LTEXT           "Use Google site:",-1,26,248,52,8
+    COMBOBOX        IDC_GOOGLE_HOME,87,245,177,256,CBS_DROPDOWNLIST |
+                    WS_VSCROLL | WS_TABSTOP
+    PUSHBUTTON      "Restore Defaults...",IDC_RESET,187,272,86,14
+END'''), 'IDD_OPTIONS_SEARCH')
+    dialog.Parse()
+    translateables = [c.GetMessage().GetRealContent()
+                      for c in dialog.GetCliques()]
+    self.failUnless('Select search buttons and options' in translateables)
+    self.failUnless('Use Google site:' in translateables)
+
+  def testAccelerators(self):
+    acc = rc.Accelerators(StringIO('''\
+IDR_ACCELERATOR1 ACCELERATORS
+BEGIN
+    "^C",           ID_ACCELERATOR32770,    ASCII,  NOINVERT
+    "^V",           ID_ACCELERATOR32771,    ASCII,  NOINVERT
+    VK_INSERT,      ID_ACCELERATOR32772,    VIRTKEY, CONTROL, NOINVERT
+END
+'''), 'IDR_ACCELERATOR1')
+    acc.Parse()
+    self.failUnless(len(acc.GetTextualIds()) == 4)
+    self.failUnless(len(acc.GetCliques()) == 0)
+
+    transl = acc.Translate('en')
+    self.failUnless(transl.strip() == acc.GetText().strip())
+
+
+  def testRegressionEmptyString(self):
+    dlg = rc.Dialog(StringIO('''\
+IDD_CONFIRM_QUIT_GD_DLG DIALOGEX 0, 0, 267, 108
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP |
+    WS_CAPTION
+EXSTYLE WS_EX_TOPMOST
+CAPTION "Google Desktop"
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+    DEFPUSHBUTTON   "&Yes",IDYES,82,87,50,14
+    PUSHBUTTON      "&No",IDNO,136,87,50,14
+    ICON            32514,IDC_STATIC,7,9,21,20
+    EDITTEXT        IDC_TEXTBOX,34,7,231,60,ES_MULTILINE | ES_READONLY | NOT WS_BORDER
+    CONTROL         "",
+                    IDC_ENABLE_GD_AUTOSTART,"Button",BS_AUTOCHECKBOX |
+                    WS_TABSTOP,33,70,231,10
+END'''), 'IDD_CONFIRM_QUIT_GD_DLG')
+    dlg.Parse()
+
+    def Check():
+      self.failUnless(transl.count('IDC_ENABLE_GD_AUTOSTART'))
+      self.failUnless(transl.count('END'))
+
+    transl = dlg.Translate('de', pseudo_if_not_available=True,
+                           fallback_to_english=True)
+    Check()
+    transl = dlg.Translate('de', pseudo_if_not_available=True,
+                           fallback_to_english=False)
+    Check()
+    transl = dlg.Translate('de', pseudo_if_not_available=False,
+                           fallback_to_english=True)
+    Check()
+    transl = dlg.Translate('de', pseudo_if_not_available=False,
+                           fallback_to_english=False)
+    Check()
+    transl = dlg.Translate('en', pseudo_if_not_available=True,
+                           fallback_to_english=True)
+    Check()
+    transl = dlg.Translate('en', pseudo_if_not_available=True,
+                           fallback_to_english=False)
+    Check()
+    transl = dlg.Translate('en', pseudo_if_not_available=False,
+                           fallback_to_english=True)
+    Check()
+    transl = dlg.Translate('en', pseudo_if_not_available=False,
+                           fallback_to_english=False)
+    Check()
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/gather/regexp.py b/tools/grit/grit/gather/regexp.py
new file mode 100644
index 0000000000..97ce2cfbf7
--- /dev/null
+++ b/tools/grit/grit/gather/regexp.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A baseclass for simple gatherers based on regular expressions.
+'''
+
+from __future__ import print_function
+
+from grit.gather import skeleton_gatherer
+
+
+class RegexpGatherer(skeleton_gatherer.SkeletonGatherer):
+  '''Common functionality of gatherers based on parsing using a single
+  regular expression.
+  '''
+
+  DescriptionMapping_ = {
+      'CAPTION' : 'This is a caption for a dialog',
+      'CHECKBOX' : 'This is a label for a checkbox',
+      'CONTROL': 'This is the text on a control',
+      'CTEXT': 'This is a label for a control',
+      'DEFPUSHBUTTON': 'This is a button definition',
+      'GROUPBOX': 'This is a label for a grouping',
+      'ICON': 'This is a label for an icon',
+      'LTEXT': 'This is the text for a label',
+      'PUSHBUTTON': 'This is the text for a button',
+    }
+
+  # Contextualization elements. Used for adding additional information
+  # to the message bundle description string from RC files.
+  def AddDescriptionElement(self, string):
+    if string in self.DescriptionMapping_:
+      description = self.DescriptionMapping_[string]
+    else:
+      description = string
+    if self.single_message_:
+      self.single_message_.SetDescription(description)
+    else:
+      if (self.translatable_chunk_):
+        message = self.skeleton_[len(self.skeleton_) - 1].GetMessage()
+        message.SetDescription(description)
+
+  def _RegExpParse(self, regexp, text_to_parse):
+    '''An implementation of Parse() that can be used for resource sections that
+    can be parsed using a single multi-line regular expression.
+
+    All translateables must be in named groups that have names starting with
+    'text'.  All textual IDs must be in named groups that have names starting
+    with 'id'. All type definitions that can be included in the description
+    field for contextualization purposes should have a name that starts with
+    'type'.
+
+    Args:
+      regexp: re.compile('...', re.MULTILINE)
+      text_to_parse:
+    '''
+    chunk_start = 0
+    for match in regexp.finditer(text_to_parse):
+      groups = match.groupdict()
+      keys = sorted(groups.keys())
+      self.translatable_chunk_ = False
+      for group in keys:
+        if group.startswith('id') and groups[group]:
+          self._AddTextualId(groups[group])
+        elif group.startswith('text') and groups[group]:
+          self._AddNontranslateableChunk(
+            text_to_parse[chunk_start : match.start(group)])
+          chunk_start = match.end(group)  # Next chunk will start after the match
+          self._AddTranslateableChunk(groups[group])
+        elif group.startswith('type') and groups[group]:
+          # Add the description to the skeleton_ list. This works because
+          # we are using a sort set of keys, and because we assume that the
+          # group name used for descriptions (type) will come after the "text"
+          # group in alphabetical order. We also assume that there cannot be
+          # more than one description per regular expression match.
+          self.AddDescriptionElement(groups[group])
+
+    self._AddNontranslateableChunk(text_to_parse[chunk_start:])
+
+    if self.single_message_:
+      self.skeleton_.append(self.uberclique.MakeClique(self.single_message_))
diff --git a/tools/grit/grit/gather/skeleton_gatherer.py b/tools/grit/grit/gather/skeleton_gatherer.py
new file mode 100644
index 0000000000..b11862b314
--- /dev/null
+++ b/tools/grit/grit/gather/skeleton_gatherer.py
@@ -0,0 +1,149 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A baseclass for simple gatherers that store their gathered resource in a
+list.
+'''
+
+from __future__ import print_function
+
+import six
+
+from grit.gather import interface
+from grit import clique
+from grit import exception
+from grit import tclib
+
+
+class SkeletonGatherer(interface.GathererBase):
+  '''Common functionality of gatherers that parse their input as a skeleton of
+  translatable and nontranslatable chunks.
+  '''
+
+  def __init__(self, *args, **kwargs):
+    super(SkeletonGatherer, self).__init__(*args, **kwargs)
+    # List of parts of the document. Translateable parts are
+    # clique.MessageClique objects, nontranslateable parts are plain strings.
+    # Translated messages are inserted back into the skeleton using the quoting
+    # rules defined by self.Escape()
+    self.skeleton_ = []
+    # A list of the names of IDs that need to be defined for this resource
+    # section to compile correctly.
+    self.ids_ = []
+    # True if Parse() has already been called.
+    self.have_parsed_ = False
+    # True if a translatable chunk has been added
+    self.translatable_chunk_ = False
+    # If not None, all parts of the document will be put into this single
+    # message; otherwise the normal skeleton approach is used.
+    self.single_message_ = None
+    # Number to use for the next placeholder name.  Used only if single_message
+    # is not None
+    self.ph_counter_ = 1
+
+  def GetText(self):
+    '''Returns the original text of the section'''
+    return self.text_
+
+  def Escape(self, text):
+    '''Subclasses can override.  Base impl is identity.
+    '''
+    return text
+
+  def UnEscape(self, text):
+    '''Subclasses can override. Base impl is identity.
+    '''
+    return text
+
+  def GetTextualIds(self):
+    '''Returns the list of textual IDs that need to be defined for this
+    resource section to compile correctly.'''
+    return self.ids_
+
+  def _AddTextualId(self, id):
+    self.ids_.append(id)
+
+  def GetCliques(self):
+    '''Returns the message cliques for each translateable message in the
+    resource section.'''
+    return [x for x in self.skeleton_ if isinstance(x, clique.MessageClique)]
+
+  def Translate(self, lang, pseudo_if_not_available=True,
+                skeleton_gatherer=None, fallback_to_english=False):
+    if len(self.skeleton_) == 0:
+      raise exception.NotReady()
+    if skeleton_gatherer:
+      assert len(skeleton_gatherer.skeleton_) == len(self.skeleton_)
+
+    out = []
+    for ix in range(len(self.skeleton_)):
+      if isinstance(self.skeleton_[ix], six.string_types):
+        if skeleton_gatherer:
+          # Make sure the skeleton is like the original
+          assert(isinstance(skeleton_gatherer.skeleton_[ix], six.string_types))
+          out.append(skeleton_gatherer.skeleton_[ix])
+        else:
+          out.append(self.skeleton_[ix])
+      else:
+        if skeleton_gatherer:  # Make sure the skeleton is like the original
+          assert(not isinstance(skeleton_gatherer.skeleton_[ix],
+                                six.string_types))
+        msg = self.skeleton_[ix].MessageForLanguage(lang,
+                                                    pseudo_if_not_available,
+                                                    fallback_to_english)
+
+        def MyEscape(text):
+          return self.Escape(text)
+        text = msg.GetRealContent(escaping_function=MyEscape)
+        out.append(text)
+    return ''.join(out)
+
+  def Parse(self):
+    '''Parses the section.  Implemented by subclasses.  Idempotent.'''
+    raise NotImplementedError()
+
+  def _AddNontranslateableChunk(self, chunk):
+    '''Adds a nontranslateable chunk.'''
+    if self.single_message_:
+      ph = tclib.Placeholder('XX%02dXX' % self.ph_counter_, chunk, chunk)
+      self.ph_counter_ += 1
+      self.single_message_.AppendPlaceholder(ph)
+    else:
+      self.skeleton_.append(chunk)
+
+  def _AddTranslateableChunk(self, chunk):
+    '''Adds a translateable chunk.  It will be unescaped before being added.'''
+    # We don't want empty messages since they are redundant and the TC
+    # doesn't allow them.
+    if chunk == '':
+      return
+
+    unescaped_text = self.UnEscape(chunk)
+    if self.single_message_:
+      self.single_message_.AppendText(unescaped_text)
+    else:
+      self.skeleton_.append(self.uberclique.MakeClique(
+        tclib.Message(text=unescaped_text)))
+      self.translatable_chunk_ = True
+
+  def SubstituteMessages(self, substituter):
+    '''Applies substitutions to all messages in the tree.
+
+    Goes through the skeleton and finds all MessageCliques.
+
+    Args:
+      substituter: a grit.util.Substituter object.
+    '''
+    if self.single_message_:
+      self.single_message_ = substituter.SubstituteMessage(self.single_message_)
+    new_skel = []
+    for chunk in self.skeleton_:
+      if isinstance(chunk, clique.MessageClique):
+        old_message = chunk.GetMessage()
+        new_message = substituter.SubstituteMessage(old_message)
+        if new_message is not old_message:
+          new_skel.append(self.uberclique.MakeClique(new_message))
+          continue
+      new_skel.append(chunk)
+    self.skeleton_ = new_skel
diff --git a/tools/grit/grit/gather/tr_html.py b/tools/grit/grit/gather/tr_html.py
new file mode 100644
index 0000000000..60a9bfaf4e
--- /dev/null
+++ b/tools/grit/grit/gather/tr_html.py
@@ -0,0 +1,743 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A gatherer for the TotalRecall brand of HTML templates with replaceable
+portions.  We wanted to reuse extern.tclib.api.handlers.html.TCHTMLParser
+but this proved impossible due to the fact that the TotalRecall HTML templates
+are in general quite far from parseable HTML and the TCHTMLParser derives
+
+from HTMLParser.HTMLParser which requires relatively well-formed HTML.  Some
+examples of "HTML" from the TotalRecall HTML templates that wouldn't be
+parseable include things like:
+
+  <a [PARAMS]>blabla</a>  (not parseable because attributes are invalid)
+
+  <table><tr><td>[LOTSOFSTUFF]</tr></table> (not parseable because closing
+                                            </td> is in the HTML [LOTSOFSTUFF]
+                                            is replaced by)
+
+The other problem with using general parsers (such as TCHTMLParser) is that
+we want to make sure we output the TotalRecall template with as little changes
+as possible in terms of whitespace characters, layout etc.  With any parser
+that generates a parse tree, and generates output by dumping the parse tree,
+we would always have little inconsistencies which could cause bugs (the
+TotalRecall template stuff is quite brittle and can break if e.g. a tab
+character is replaced with spaces).
+
+The solution, which may be applicable to some other HTML-like template
+languages floating around Google, is to create a parser with a simple state
+machine that keeps track of what kind of tag it's inside, and whether it's in
+a translateable section or not.  Translateable sections are:
+
+a) text (including [BINGO] replaceables) inside of tags that
+   can contain translateable text (which is all tags except
+   for a few)
+
+b) text inside of an 'alt' attribute in an <image> element, or
+   the 'value' attribute of a <submit>, <button> or <text>
+   element.
+
+The parser does not build up a parse tree but rather a "skeleton" which
+is a list of nontranslateable strings intermingled with grit.clique.MessageClique
+objects.  This simplifies the parser considerably compared to a regular HTML
+parser.  To output a translated document, each item in the skeleton is
+printed out, with the relevant Translation from each MessageCliques being used
+for the requested language.
+
+This implementation borrows some code, constants and ideas from
+extern.tclib.api.handlers.html.TCHTMLParser.
+'''
+
+from __future__ import print_function
+
+import re
+
+import six
+
+from grit import clique
+from grit import exception
+from grit import lazy_re
+from grit import util
+from grit import tclib
+
+from grit.gather import interface
+
+
+# HTML tags which break (separate) chunks.
+_BLOCK_TAGS = ['script', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'br',
+              'body', 'style', 'head', 'title', 'table', 'tr', 'td', 'th',
+              'ul', 'ol', 'dl', 'nl', 'li', 'div', 'object', 'center',
+              'html', 'link', 'form', 'select', 'textarea',
+              'button', 'option', 'map', 'area', 'blockquote', 'pre',
+              'meta', 'xmp', 'noscript', 'label', 'tbody', 'thead',
+              'script', 'style', 'pre', 'iframe', 'img', 'input', 'nowrap',
+              'fieldset', 'legend']
+
+# HTML tags which may appear within a chunk.
+_INLINE_TAGS = ['b', 'i', 'u', 'tt', 'code', 'font', 'a', 'span', 'small',
+               'key', 'nobr', 'url', 'em', 's', 'sup', 'strike',
+               'strong']
+
+# HTML tags within which linebreaks are significant.
+_PREFORMATTED_TAGS = ['textarea', 'xmp', 'pre']
+
+# An array mapping some of the inline HTML tags to more meaningful
+# names for those tags.  This will be used when generating placeholders
+# representing these tags.
+_HTML_PLACEHOLDER_NAMES = { 'a' : 'link', 'br' : 'break', 'b' : 'bold',
+  'i' : 'italic', 'li' : 'item', 'ol' : 'ordered_list', 'p' : 'paragraph',
+  'ul' : 'unordered_list', 'img' : 'image', 'em' : 'emphasis' }
+
+# We append each of these characters in sequence to distinguish between
+# different placeholders with basically the same name (e.g. BOLD1, BOLD2).
+# Keep in mind that a placeholder name must not be a substring of any other
+# placeholder name in the same message, so we can't simply count (BOLD_1
+# would be a substring of BOLD_10).
+_SUFFIXES = '123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+
+# Matches whitespace in an HTML document.  Also matches HTML comments, which are
+# treated as whitespace.
+_WHITESPACE = lazy_re.compile(r'(\s| |\\n|\\r|<!--\s*desc\s*=.*?-->)+',
+                              re.DOTALL)
+
+# Matches whitespace sequences which can be folded into a single whitespace
+# character.  This matches single characters so that non-spaces are replaced
+# with spaces.
+_FOLD_WHITESPACE = lazy_re.compile(r'\s+')
+
+# Finds a non-whitespace character
+_NON_WHITESPACE = lazy_re.compile(r'\S')
+
+# Matches two or more   in a row (a single   is not changed into
+# placeholders because different languages require different numbers of spaces
+# and placeholders must match exactly; more than one is probably a "special"
+# whitespace sequence and should be turned into a placeholder).
+_NBSP = lazy_re.compile(r' ( )+')
+
+# Matches nontranslateable chunks of the document
+_NONTRANSLATEABLES = lazy_re.compile(r'''
+  <\s*script.+?<\s*/\s*script\s*>
+  |
+  <\s*style.+?<\s*/\s*style\s*>
+  |
+  <!--.+?-->
+  |
+  <\?IMPORT\s.+?>           # import tag
+  |
+  <\s*[a-zA-Z_]+:.+?>       # custom tag (open)
+  |
+  <\s*/\s*[a-zA-Z_]+:.+?>   # custom tag (close)
+  |
+  <!\s*[A-Z]+\s*([^>]+|"[^"]+"|'[^']+')*?>
+  ''', re.MULTILINE | re.DOTALL | re.VERBOSE | re.IGNORECASE)
+
+# Matches a tag and its attributes
+_ELEMENT = lazy_re.compile(r'''
+  # Optional closing /, element name
+  <\s*(?P<closing>/)?\s*(?P<element>[a-zA-Z0-9]+)\s*
+  # Attributes and/or replaceables inside the tag, if any
+  (?P<atts>(
+    \s*([a-zA-Z_][-:.a-zA-Z_0-9]*) # Attribute name
+    (\s*=\s*(\'[^\']*\'|"[^"]*"|[-a-zA-Z0-9./,:;+*%?!&$\(\)_#=~\'"@]*))?
+    |
+    \s*\[(\$?\~)?([A-Z0-9-_]+?)(\~\$?)?\]
+  )*)
+  \s*(?P<empty>/)?\s*> # Optional empty-tag closing /, and tag close
+  ''',
+  re.MULTILINE | re.DOTALL | re.VERBOSE)
+
+# Matches elements that may have translateable attributes.  The value of these
+# special attributes is given by group 'value1' or 'value2'.  Note that this
+# regexp demands that the attribute value be quoted; this is necessary because
+# the non-tree-building nature of the parser means we don't know when we're
+# writing out attributes, so we wouldn't know to escape spaces.
+_SPECIAL_ELEMENT = lazy_re.compile(r'''
+  <\s*(
+    input[^>]+?value\s*=\s*(\'(?P<value3>[^\']*)\'|"(?P<value4>[^"]*)")
+    [^>]+type\s*=\s*"?'?(button|reset|text|submit)'?"?
+    |
+    (
+      table[^>]+?title\s*=
+      |
+      img[^>]+?alt\s*=
+      |
+      input[^>]+?type\s*=\s*"?'?(button|reset|text|submit)'?"?[^>]+?value\s*=
+    )
+    \s*(\'(?P<value1>[^\']*)\'|"(?P<value2>[^"]*)")
+  )[^>]*?>
+  ''', re.MULTILINE | re.DOTALL | re.VERBOSE | re.IGNORECASE)
+
+# Matches stuff that is translateable if it occurs in the right context
+# (between tags).  This includes all characters and character entities.
+# Note that this also matches   which needs to be handled as whitespace
+# before this regexp is applied.
+_CHARACTERS = lazy_re.compile(r'''
+  (
+    \w
+    |
+    [\!\@\#\$\%\^\*\(\)\-\=\_\+\[\]\{\}\\\|\;\:\'\"\,\.\/\?\`\~]
+    |
+    &(\#[0-9]+|\#x[0-9a-fA-F]+|[A-Za-z0-9]+);
+  )+
+  ''', re.MULTILINE | re.DOTALL | re.VERBOSE)
+
+# Matches Total Recall's "replaceable" tags, which are just any text
+# in capitals enclosed by delimiters like [] or [~~] or [$~~$] (e.g. [HELLO],
+# [~HELLO~] and [$~HELLO~$]).
+_REPLACEABLE = lazy_re.compile(r'\[(\$?\~)?(?P<name>[A-Z0-9-_]+?)(\~\$?)?\]',
+                               re.MULTILINE)
+
+
+# Matches the silly [!]-prefixed "header" that is used in some TotalRecall
+# templates.
+_SILLY_HEADER = lazy_re.compile(r'\[!\]\ntitle\t(?P<title>[^\n]+?)\n.+?\n\n',
+                                re.MULTILINE | re.DOTALL)
+
+
+# Matches a comment that provides a description for the message it occurs in.
+_DESCRIPTION_COMMENT = lazy_re.compile(
+  r'<!--\s*desc\s*=\s*(?P<description>.+?)\s*-->', re.DOTALL)
+
+# Matches a comment which is used to break apart multiple messages.
+_MESSAGE_BREAK_COMMENT = lazy_re.compile(r'<!--\s*message-break\s*-->',
+                                         re.DOTALL)
+
+# Matches a comment which is used to prevent block tags from splitting a message
+_MESSAGE_NO_BREAK_COMMENT = re.compile(r'<!--\s*message-no-break\s*-->',
+                                       re.DOTALL)
+
+
+_DEBUG = 0
+def _DebugPrint(text):
+  if _DEBUG:
+    print(text.encode('utf-8'))
+
+
+class HtmlChunks(object):
+  '''A parser that knows how to break an HTML-like document into a list of
+  chunks, where each chunk is either translateable or non-translateable.
+  The chunks are unmodified sections of the original document, so concatenating
+  the text of all chunks would result in the original document.'''
+
+  def InTranslateable(self):
+    return self.last_translateable != -1
+
+  def Rest(self):
+    return self.text_[self.current:]
+
+  def StartTranslateable(self):
+    assert not self.InTranslateable()
+    if self.current != 0:
+      # Append a nontranslateable chunk
+      chunk_text = self.text_[self.chunk_start : self.last_nontranslateable + 1]
+      # Needed in the case where document starts with a translateable.
+      if len(chunk_text) > 0:
+        self.AddChunk(False, chunk_text)
+    self.chunk_start = self.last_nontranslateable + 1
+    self.last_translateable = self.current
+    self.last_nontranslateable = -1
+
+  def EndTranslateable(self):
+    assert self.InTranslateable()
+    # Append a translateable chunk
+    self.AddChunk(True,
+                  self.text_[self.chunk_start : self.last_translateable + 1])
+    self.chunk_start = self.last_translateable + 1
+    self.last_translateable = -1
+    self.last_nontranslateable = self.current
+
+  def AdvancePast(self, match):
+    self.current += match.end()
+
+  def AddChunk(self, translateable, text):
+    '''Adds a chunk to self, removing linebreaks and duplicate whitespace
+    if appropriate.
+    '''
+    m = _DESCRIPTION_COMMENT.search(text)
+    if m:
+      self.last_description = m.group('description')
+      # Remove the description from the output text
+      text = _DESCRIPTION_COMMENT.sub('', text)
+
+    m = _MESSAGE_BREAK_COMMENT.search(text)
+    if m:
+      # Remove the coment from the output text.  It should already effectively
+      # break apart messages.
+      text = _MESSAGE_BREAK_COMMENT.sub('', text)
+
+    if translateable and not self.last_element_ in _PREFORMATTED_TAGS:
+      if self.fold_whitespace_:
+        # Fold whitespace sequences if appropriate.  This is optional because it
+        # alters the output strings.
+        text = _FOLD_WHITESPACE.sub(' ', text)
+      else:
+        text = text.replace('\n', ' ')
+        text = text.replace('\r', ' ')
+        # This whitespace folding doesn't work in all cases, thus the
+        # fold_whitespace flag to support backwards compatibility.
+        text = text.replace('   ', ' ')
+        text = text.replace('  ', ' ')
+
+    if translateable:
+      description = self.last_description
+      self.last_description = ''
+    else:
+      description = ''
+
+    if text != '':
+      self.chunks_.append((translateable, text, description))
+
+  def Parse(self, text, fold_whitespace):
+    '''Parses self.text_ into an intermediate format stored in self.chunks_
+    which is translateable and nontranslateable chunks.  Also returns
+    self.chunks_
+
+    Args:
+      text: The HTML for parsing.
+      fold_whitespace: Whether whitespace sequences should be folded into a
+        single space.
+
+    Return:
+      [chunk1, chunk2, chunk3, ...]  (instances of class Chunk)
+    '''
+    #
+    # Chunker state
+    #
+
+    self.text_ = text
+    self.fold_whitespace_ = fold_whitespace
+
+    # A list of tuples (is_translateable, text) which represents the document
+    # after chunking.
+    self.chunks_ = []
+
+    # Start index of the last chunk, whether translateable or not
+    self.chunk_start = 0
+
+    # Index of the last for-sure translateable character if we are parsing
+    # a translateable chunk, -1 to indicate we are not in a translateable chunk.
+    # This is needed so that we don't include trailing whitespace in the
+    # translateable chunk (whitespace is neutral).
+    self.last_translateable = -1
+
+    # Index of the last for-sure nontranslateable character if we are parsing
+    # a nontranslateable chunk, -1 if we are not in a nontranslateable chunk.
+    # This is needed to make sure we can group e.g. "<b>Hello</b> there"
+    # together instead of just "Hello</b> there" which would be much worse
+    # for translation.
+    self.last_nontranslateable = -1
+
+    # Index of the character we're currently looking at.
+    self.current = 0
+
+    # The name of the last block element parsed.
+    self.last_element_ = ''
+
+    # The last explicit description we found.
+    self.last_description = ''
+
+    # Whether no-break was the last chunk seen
+    self.last_nobreak = False
+
+    while self.current < len(self.text_):
+      _DebugPrint('REST: %s' % self.text_[self.current:self.current+60])
+
+      m = _MESSAGE_NO_BREAK_COMMENT.match(self.Rest())
+      if m:
+        self.AdvancePast(m)
+        self.last_nobreak = True
+        continue
+
+      # Try to match whitespace
+      m = _WHITESPACE.match(self.Rest())
+      if m:
+        # Whitespace is neutral, it just advances 'current' and does not switch
+        # between translateable/nontranslateable.  If we are in a
+        # nontranslateable section that extends to the current point, we extend
+        # it to include the whitespace.  If we are in a translateable section,
+        # we do not extend it until we find
+        # more translateable parts, because we never want a translateable chunk
+        # to end with whitespace.
+        if (not self.InTranslateable() and
+            self.last_nontranslateable == self.current - 1):
+          self.last_nontranslateable = self.current + m.end() - 1
+        self.AdvancePast(m)
+        continue
+
+      # Then we try to match nontranslateables
+      m = _NONTRANSLATEABLES.match(self.Rest())
+      if m:
+        if self.InTranslateable():
+          self.EndTranslateable()
+        self.last_nontranslateable = self.current + m.end() - 1
+        self.AdvancePast(m)
+        continue
+
+      # Now match all other HTML element tags (opening, closing, or empty, we
+      # don't care).
+      m = _ELEMENT.match(self.Rest())
+      if m:
+        element_name = m.group('element').lower()
+        if element_name in _BLOCK_TAGS:
+          self.last_element_ = element_name
+          if self.InTranslateable():
+            if self.last_nobreak:
+              self.last_nobreak = False
+            else:
+              self.EndTranslateable()
+
+          # Check for "special" elements, i.e. ones that have a translateable
+          # attribute, and handle them correctly.  Note that all of the
+          # "special" elements are block tags, so no need to check for this
+          # if the tag is not a block tag.
+          sm = _SPECIAL_ELEMENT.match(self.Rest())
+          if sm:
+            # Get the appropriate group name
+            for group in sm.groupdict():
+              if sm.groupdict()[group]:
+                break
+
+            # First make a nontranslateable chunk up to and including the
+            # quote before the translateable attribute value
+            self.AddChunk(False, self.text_[
+              self.chunk_start : self.current + sm.start(group)])
+            # Then a translateable for the translateable bit
+            self.AddChunk(True, self.Rest()[sm.start(group) : sm.end(group)])
+            # Finally correct the data invariant for the parser
+            self.chunk_start = self.current + sm.end(group)
+
+          self.last_nontranslateable = self.current + m.end() - 1
+        elif self.InTranslateable():
+          # We're in a translateable and the tag is an inline tag, so we
+          # need to include it in the translateable.
+          self.last_translateable = self.current + m.end() - 1
+        self.AdvancePast(m)
+        continue
+
+      # Anything else we find must be translateable, so we advance one character
+      # at a time until one of the above matches.
+      if not self.InTranslateable():
+        self.StartTranslateable()
+      else:
+        self.last_translateable = self.current
+      self.current += 1
+
+    # Close the final chunk
+    if self.InTranslateable():
+      self.AddChunk(True, self.text_[self.chunk_start : ])
+    else:
+      self.AddChunk(False, self.text_[self.chunk_start : ])
+
+    return self.chunks_
+
+
+def HtmlToMessage(html, include_block_tags=False, description=''):
+  '''Takes a bit of HTML, which must contain only "inline" HTML elements,
+  and changes it into a tclib.Message.  This involves escaping any entities and
+  replacing any HTML code with placeholders.
+
+  If include_block_tags is true, no error will be given if block tags (e.g.
+  <p> or <br>) are included in the HTML.
+
+  Args:
+    html: 'Hello <b>[USERNAME]</b>, how <i>are</i> you?'
+    include_block_tags: False
+
+  Return:
+    tclib.Message('Hello START_BOLD1USERNAMEEND_BOLD, '
+                  'howNBSPSTART_ITALICareEND_ITALIC you?',
+                  [ Placeholder('START_BOLD', '<b>', ''),
+                    Placeholder('USERNAME', '[USERNAME]', ''),
+                    Placeholder('END_BOLD', '</b>', ''),
+                    Placeholder('START_ITALIC', '<i>', ''),
+                    Placeholder('END_ITALIC', '</i>', ''), ])
+  '''
+  # Approach is:
+  # - first placeholderize, finding <elements>, [REPLACEABLES] and  
+  # - then escape all character entities in text in-between placeholders
+
+  parts = []  # List of strings (for text chunks) and tuples (ID, original)
+              # for placeholders
+
+  count_names = {}  # Map of base names to number of times used
+  end_names = {}  # Map of base names to stack of end tags (for correct nesting)
+
+  def MakeNameClosure(base, type = ''):
+    '''Returns a closure that can be called once all names have been allocated
+    to return the final name of the placeholder.  This allows us to minimally
+    number placeholders for non-overlap.
+
+    Also ensures that END_XXX_Y placeholders have the same Y as the
+    corresponding BEGIN_XXX_Y placeholder when we have nested tags of the same
+    type.
+
+    Args:
+      base: 'phname'
+      type: '' | 'begin' | 'end'
+
+    Return:
+      Closure()
+    '''
+    name = base.upper()
+    if type != '':
+      name = ('%s_%s' % (type, base)).upper()
+
+    count_names.setdefault(name, 0)
+    count_names[name] += 1
+
+    def MakeFinalName(name_ = name, index = count_names[name] - 1):
+      if type.lower() == 'end' and end_names.get(base):
+        return end_names[base].pop(-1)  # For correct nesting
+      if count_names[name_] != 1:
+        name_ = '%s_%s' % (name_, _SUFFIXES[index])
+        # We need to use a stack to ensure that the end-tag suffixes match
+        # the begin-tag suffixes.  Only needed when more than one tag of the
+        # same type.
+        if type == 'begin':
+          end_name = ('END_%s_%s' % (base, _SUFFIXES[index])).upper()
+          if base in end_names:
+            end_names[base].append(end_name)
+          else:
+            end_names[base] = [end_name]
+
+      return name_
+
+    return MakeFinalName
+
+  current = 0
+  last_nobreak = False
+
+  while current < len(html):
+    m = _MESSAGE_NO_BREAK_COMMENT.match(html[current:])
+    if m:
+      last_nobreak = True
+      current += m.end()
+      continue
+
+    m = _NBSP.match(html[current:])
+    if m:
+      parts.append((MakeNameClosure('SPACE'), m.group()))
+      current += m.end()
+      continue
+
+    m = _REPLACEABLE.match(html[current:])
+    if m:
+      # Replaceables allow - but placeholders don't, so replace - with _
+      ph_name = MakeNameClosure('X_%s_X' % m.group('name').replace('-', '_'))
+      parts.append((ph_name, m.group()))
+      current += m.end()
+      continue
+
+    m = _SPECIAL_ELEMENT.match(html[current:])
+    if m:
+      if not include_block_tags:
+        if last_nobreak:
+          last_nobreak = False
+        else:
+          raise exception.BlockTagInTranslateableChunk(html)
+      element_name = 'block'  # for simplification
+      # Get the appropriate group name
+      for group in m.groupdict():
+        if m.groupdict()[group]:
+          break
+      parts.append((MakeNameClosure(element_name, 'begin'),
+                    html[current : current + m.start(group)]))
+      parts.append(m.group(group))
+      parts.append((MakeNameClosure(element_name, 'end'),
+                    html[current + m.end(group) : current + m.end()]))
+      current += m.end()
+      continue
+
+    m = _ELEMENT.match(html[current:])
+    if m:
+      element_name = m.group('element').lower()
+      if not include_block_tags and not element_name in _INLINE_TAGS:
+        if last_nobreak:
+          last_nobreak = False
+        else:
+          raise exception.BlockTagInTranslateableChunk(html[current:])
+      if element_name in _HTML_PLACEHOLDER_NAMES:  # use meaningful names
+        element_name = _HTML_PLACEHOLDER_NAMES[element_name]
+
+      # Make a name for the placeholder
+      type = ''
+      if not m.group('empty'):
+        if m.group('closing'):
+          type = 'end'
+        else:
+          type = 'begin'
+      parts.append((MakeNameClosure(element_name, type), m.group()))
+      current += m.end()
+      continue
+
+    if len(parts) and isinstance(parts[-1], six.string_types):
+      parts[-1] += html[current]
+    else:
+      parts.append(html[current])
+    current += 1
+
+  msg_text = ''
+  placeholders = []
+  for part in parts:
+    if isinstance(part, tuple):
+      final_name = part[0]()
+      original = part[1]
+      msg_text += final_name
+      placeholders.append(tclib.Placeholder(final_name, original, '(HTML code)'))
+    else:
+      msg_text += part
+
+  msg = tclib.Message(text=msg_text, placeholders=placeholders,
+                      description=description)
+  content = msg.GetContent()
+  for ix in range(len(content)):
+    if isinstance(content[ix], six.string_types):
+      content[ix] = util.UnescapeHtml(content[ix], replace_nbsp=False)
+
+  return msg
+
+
+class TrHtml(interface.GathererBase):
+  '''Represents a document or message in the template format used by
+  Total Recall for HTML documents.'''
+
+  def __init__(self, *args, **kwargs):
+    super(TrHtml, self).__init__(*args, **kwargs)
+    self.have_parsed_ = False
+    self.skeleton_ = []  # list of strings and MessageClique objects
+    self.fold_whitespace_ = False
+
+  def SetAttributes(self, attrs):
+    '''Sets node attributes used by the gatherer.
+
+    This checks the fold_whitespace attribute.
+
+    Args:
+      attrs: The mapping of node attributes.
+    '''
+    self.fold_whitespace_ = ('fold_whitespace' in attrs and
+                             attrs['fold_whitespace'] == 'true')
+
+  def GetText(self):
+    '''Returns the original text of the HTML document'''
+    return self.text_
+
+  def GetTextualIds(self):
+    return [self.extkey]
+
+  def GetCliques(self):
+    '''Returns the message cliques for each translateable message in the
+    document.'''
+    return [x for x in self.skeleton_ if isinstance(x, clique.MessageClique)]
+
+  def Translate(self, lang, pseudo_if_not_available=True,
+                skeleton_gatherer=None, fallback_to_english=False):
+    '''Returns this document with translateable messages filled with
+    the translation for language 'lang'.
+
+    Args:
+      lang: 'en'
+      pseudo_if_not_available: True
+
+    Return:
+      'ID_THIS_SECTION TYPE\n...BEGIN\n  "Translated message"\n......\nEND
+
+    Raises:
+      grit.exception.NotReady() if used before Parse() has been successfully
+      called.
+      grit.exception.NoSuchTranslation() if 'pseudo_if_not_available' is false
+      and there is no translation for the requested language.
+    '''
+    if len(self.skeleton_) == 0:
+      raise exception.NotReady()
+
+    # TODO(joi) Implement support for skeleton gatherers here.
+
+    out = []
+    for item in self.skeleton_:
+      if isinstance(item, six.string_types):
+        out.append(item)
+      else:
+        msg = item.MessageForLanguage(lang,
+                                      pseudo_if_not_available,
+                                      fallback_to_english)
+        for content in msg.GetContent():
+          if isinstance(content, tclib.Placeholder):
+            out.append(content.GetOriginal())
+          else:
+            # We escape " characters to increase the chance that attributes
+            # will be properly escaped.
+            out.append(util.EscapeHtml(content, True))
+
+    return ''.join(out)
+
+  def Parse(self):
+    if self.have_parsed_:
+      return
+    self.have_parsed_ = True
+
+    text = self._LoadInputFile()
+
+    # Ignore the BOM character if the document starts with one.
+    if text.startswith(u'\ufeff'):
+      text = text[1:]
+
+    self.text_ = text
+
+    # Parsing is done in two phases:  First, we break the document into
+    # translateable and nontranslateable chunks.  Second, we run through each
+    # translateable chunk and insert placeholders for any HTML elements,
+    # unescape escaped characters, etc.
+
+    # First handle the silly little [!]-prefixed header because it's not
+    # handled by our HTML parsers.
+    m = _SILLY_HEADER.match(text)
+    if m:
+      self.skeleton_.append(text[:m.start('title')])
+      self.skeleton_.append(self.uberclique.MakeClique(
+        tclib.Message(text=text[m.start('title'):m.end('title')])))
+      self.skeleton_.append(text[m.end('title') : m.end()])
+      text = text[m.end():]
+
+    chunks = HtmlChunks().Parse(text, self.fold_whitespace_)
+
+    for chunk in chunks:
+      if chunk[0]:  # Chunk is translateable
+        self.skeleton_.append(self.uberclique.MakeClique(
+          HtmlToMessage(chunk[1], description=chunk[2])))
+      else:
+        self.skeleton_.append(chunk[1])
+
+    # Go through the skeleton and change any messages that consist solely of
+    # placeholders and whitespace into nontranslateable strings.
+    for ix in range(len(self.skeleton_)):
+      got_text = False
+      if isinstance(self.skeleton_[ix], clique.MessageClique):
+        msg = self.skeleton_[ix].GetMessage()
+        for item in msg.GetContent():
+          if (isinstance(item, six.string_types)
+              and _NON_WHITESPACE.search(item) and item != ' '):
+            got_text = True
+            break
+        if not got_text:
+          self.skeleton_[ix] = msg.GetRealContent()
+
+  def SubstituteMessages(self, substituter):
+    '''Applies substitutions to all messages in the tree.
+
+    Goes through the skeleton and finds all MessageCliques.
+
+    Args:
+      substituter: a grit.util.Substituter object.
+    '''
+    new_skel = []
+    for chunk in self.skeleton_:
+      if isinstance(chunk, clique.MessageClique):
+        old_message = chunk.GetMessage()
+        new_message = substituter.SubstituteMessage(old_message)
+        if new_message is not old_message:
+          new_skel.append(self.uberclique.MakeClique(new_message))
+          continue
+      new_skel.append(chunk)
+    self.skeleton_ = new_skel
diff --git a/tools/grit/grit/gather/tr_html_unittest.py b/tools/grit/grit/gather/tr_html_unittest.py
new file mode 100644
index 0000000000..1194853d9a
--- /dev/null
+++ b/tools/grit/grit/gather/tr_html_unittest.py
@@ -0,0 +1,524 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.gather.tr_html'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+import six
+from six import StringIO
+
+from grit.gather import tr_html
+from grit import clique
+from grit import util
+
+
+class ParserUnittest(unittest.TestCase):
+  def testChunkingWithoutFoldWhitespace(self):
+    self.VerifyChunking(False)
+
+  def testChunkingWithFoldWhitespace(self):
+    self.VerifyChunking(True)
+
+  def VerifyChunking(self, fold_whitespace):
+    """Use a single function to run all chunking testing.
+
+    This makes it easier to run chunking with fold_whitespace both on and off,
+    to make sure the outputs are the same.
+
+    Args:
+      fold_whitespace: Whether whitespace sequences should be folded into a
+        single space.
+    """
+    self.VerifyChunkingBasic(fold_whitespace)
+    self.VerifyChunkingDescriptions(fold_whitespace)
+    self.VerifyChunkingReplaceables(fold_whitespace)
+    self.VerifyChunkingLineBreaks(fold_whitespace)
+    self.VerifyChunkingMessageBreak(fold_whitespace)
+    self.VerifyChunkingMessageNoBreak(fold_whitespace)
+
+  def VerifyChunkingBasic(self, fold_whitespace):
+    p = tr_html.HtmlChunks()
+    chunks = p.Parse('<p>Hello <b>dear</b> how <i>are</i>you?<p>Fine!',
+                     fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (False, '<p>', ''), (True, 'Hello <b>dear</b> how <i>are</i>you?', ''),
+      (False, '<p>', ''), (True, 'Fine!', '')])
+
+    chunks = p.Parse('<p> Hello <b>dear</b> how <i>are</i>you? <p>Fine!',
+                     fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (False, '<p> ', ''), (True, 'Hello <b>dear</b> how <i>are</i>you?', ''),
+      (False, ' <p>', ''), (True, 'Fine!', '')])
+
+    chunks = p.Parse('<p> Hello <b>dear how <i>are you? <p> Fine!',
+                     fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (False, '<p> ', ''), (True, 'Hello <b>dear how <i>are you?', ''),
+      (False, ' <p> ', ''), (True, 'Fine!', '')])
+
+    # Ensure translateable sections that start with inline tags contain
+    # the starting inline tag.
+    chunks = p.Parse('<b>Hello!</b> how are you?<p><i>I am fine.</i>',
+                     fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (True, '<b>Hello!</b> how are you?', ''), (False, '<p>', ''),
+      (True, '<i>I am fine.</i>', '')])
+
+    # Ensure translateable sections that end with inline tags contain
+    # the ending inline tag.
+    chunks = p.Parse("Hello! How are <b>you?</b><p><i>I'm fine!</i>",
+                     fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (True, 'Hello! How are <b>you?</b>', ''), (False, '<p>', ''),
+      (True, "<i>I'm fine!</i>", '')])
+
+  def VerifyChunkingDescriptions(self, fold_whitespace):
+    p = tr_html.HtmlChunks()
+    # Check capitals and explicit descriptions
+    chunks = p.Parse('<!-- desc=bingo! --><B>Hello!</B> how are you?<P>'
+                     '<I>I am fine.</I>', fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (True, '<B>Hello!</B> how are you?', 'bingo!'), (False, '<P>', ''),
+      (True, '<I>I am fine.</I>', '')])
+    chunks = p.Parse('<B><!-- desc=bingo! -->Hello!</B> how are you?<P>'
+                     '<I>I am fine.</I>', fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (True, '<B>Hello!</B> how are you?', 'bingo!'), (False, '<P>', ''),
+      (True, '<I>I am fine.</I>', '')])
+    # Linebreaks get handled by the tclib message.
+    chunks = p.Parse('<B>Hello!</B> <!-- desc=bi\nngo\n! -->how are you?<P>'
+                     '<I>I am fine.</I>', fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (True, '<B>Hello!</B> how are you?', 'bi\nngo\n!'), (False, '<P>', ''),
+      (True, '<I>I am fine.</I>', '')])
+
+    # In this case, because the explicit description appears after the first
+    # translateable, it will actually apply to the second translateable.
+    chunks = p.Parse('<B>Hello!</B> how are you?<!-- desc=bingo! --><P>'
+                     '<I>I am fine.</I>', fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (True, '<B>Hello!</B> how are you?', ''), (False, '<P>', ''),
+      (True, '<I>I am fine.</I>', 'bingo!')])
+
+  def VerifyChunkingReplaceables(self, fold_whitespace):
+    # Check that replaceables within block tags (where attributes would go) are
+    # handled correctly.
+    p = tr_html.HtmlChunks()
+    chunks = p.Parse('<b>Hello!</b> how are you?<p [BINGO] [$~BONGO~$]>'
+                     '<i>I am fine.</i>', fold_whitespace)
+    self.failUnlessEqual(chunks, [
+      (True, '<b>Hello!</b> how are you?', ''),
+      (False, '<p [BINGO] [$~BONGO~$]>', ''),
+      (True, '<i>I am fine.</i>', '')])
+
+  def VerifyChunkingLineBreaks(self, fold_whitespace):
+    # Check that the contents of preformatted tags preserve line breaks.
+    p = tr_html.HtmlChunks()
+    chunks = p.Parse('<textarea>Hello\nthere\nhow\nare\nyou?</textarea>',
+                     fold_whitespace)
+    self.failUnlessEqual(chunks, [(False, '<textarea>', ''),
+      (True, 'Hello\nthere\nhow\nare\nyou?', ''), (False, '</textarea>', '')])
+
+    # ...and that other tags' line breaks are converted to spaces
+    chunks = p.Parse('<p>Hello\nthere\nhow\nare\nyou?</p>', fold_whitespace)
+    self.failUnlessEqual(chunks, [(False, '<p>', ''),
+      (True, 'Hello there how are you?', ''), (False, '</p>', '')])
+
+  def VerifyChunkingMessageBreak(self, fold_whitespace):
+    p = tr_html.HtmlChunks()
+    # Make sure that message-break comments work properly.
+    chunks = p.Parse('Break<!-- message-break --> apart '
+                     '<!--message-break-->messages', fold_whitespace)
+    self.failUnlessEqual(chunks, [(True, 'Break', ''),
+                                  (False, ' ', ''),
+                                  (True, 'apart', ''),
+                                  (False, ' ', ''),
+                                  (True, 'messages', '')])
+
+    # Make sure message-break comments work in an inline tag.
+    chunks = p.Parse('<a href=\'google.com\'><!-- message-break -->Google'
+                     '<!--message-break--></a>', fold_whitespace)
+    self.failUnlessEqual(chunks, [(False, '<a href=\'google.com\'>', ''),
+                                  (True, 'Google', ''),
+                                  (False, '</a>', '')])
+
+  def VerifyChunkingMessageNoBreak(self, fold_whitespace):
+    p = tr_html.HtmlChunks()
+    # Make sure that message-no-break comments work properly.
+    chunks = p.Parse('Please <!-- message-no-break --> <br />don\'t break',
+                     fold_whitespace)
+    self.failUnlessEqual(chunks, [(True, 'Please <!-- message-no-break --> '
+                         '<br />don\'t break', '')])
+
+    chunks = p.Parse('Please <br /> break. <!-- message-no-break --> <br /> '
+                     'But not this time.', fold_whitespace)
+    self.failUnlessEqual(chunks, [(True, 'Please', ''),
+                                  (False, ' <br /> ', ''),
+                                  (True, 'break. <!-- message-no-break --> '
+                                         '<br /> But not this time.', '')])
+
+  def testTranslateableAttributes(self):
+    p = tr_html.HtmlChunks()
+
+    # Check that the translateable attributes in <img>, <submit>, <button> and
+    # <text> elements buttons are handled correctly.
+    chunks = p.Parse('<img src=bingo.jpg alt="hello there">'
+                     '<input type=submit value="hello">'
+                     '<input type="button" value="hello">'
+                     '<input type=\'text\' value=\'Howdie\'>', False)
+    self.failUnlessEqual(chunks, [
+      (False, '<img src=bingo.jpg alt="', ''), (True, 'hello there', ''),
+      (False, '"><input type=submit value="', ''), (True, 'hello', ''),
+      (False, '"><input type="button" value="', ''), (True, 'hello', ''),
+      (False, '"><input type=\'text\' value=\'', ''), (True, 'Howdie', ''),
+      (False, '\'>', '')])
+
+
+  def testTranslateableHtmlToMessage(self):
+    msg = tr_html.HtmlToMessage(
+      'Hello <b>[USERNAME]</b>, <how> <i>are</i> you?')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres ==
+                    'Hello BEGIN_BOLDX_USERNAME_XEND_BOLD, '
+                    '<how> BEGIN_ITALICareEND_ITALIC you?')
+
+    msg = tr_html.HtmlToMessage('<b>Hello</b><I>Hello</I><b>Hello</b>')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres ==
+                    'BEGIN_BOLD_1HelloEND_BOLD_1BEGIN_ITALICHelloEND_ITALIC'
+                    'BEGIN_BOLD_2HelloEND_BOLD_2')
+
+    # Check that nesting (of the <font> tags) is handled correctly - i.e. that
+    # the closing placeholder numbers match the opening placeholders.
+    msg = tr_html.HtmlToMessage(
+      '''<font size=-1><font color=#FF0000>Update!</font> '''
+      '''<a href='http://desktop.google.com/whatsnew.html?hl=[$~LANG~$]'>'''
+      '''New Features</a>: Now search PDFs, MP3s, Firefox web history, and '''
+      '''more</font>''')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres ==
+                    'BEGIN_FONT_1BEGIN_FONT_2Update!END_FONT_2 BEGIN_LINK'
+                    'New FeaturesEND_LINK: Now search PDFs, MP3s, Firefox '
+                    'web history, and moreEND_FONT_1')
+
+    msg = tr_html.HtmlToMessage('''<a href='[$~URL~$]'><b>[NUM][CAT]</b></a>''')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres == 'BEGIN_LINKBEGIN_BOLDX_NUM_XX_CAT_XEND_BOLDEND_LINK')
+
+    msg = tr_html.HtmlToMessage(
+      '''<font size=-1><a class=q onClick='return window.qs?qs(this):1' '''
+      '''href='http://[WEBSERVER][SEARCH_URI]'>Desktop</a></font>  '''
+      '''  ''')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres ==
+                    '''BEGIN_FONTBEGIN_LINKDesktopEND_LINKEND_FONTSPACE''')
+
+    msg = tr_html.HtmlToMessage(
+      '''<br><br><center><font size=-2>©2005 Google </font></center>''', 1)
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres ==
+                    u'BEGIN_BREAK_1BEGIN_BREAK_2BEGIN_CENTERBEGIN_FONT\xa92005'
+                    u' Google END_FONTEND_CENTER')
+
+    msg = tr_html.HtmlToMessage(
+      ''' - <a class=c href=[$~CACHE~$]>Cached</a>''')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres ==
+                    ' - BEGIN_LINKCachedEND_LINK')
+
+    # Check that upper-case tags are handled correctly.
+    msg = tr_html.HtmlToMessage(
+      '''You can read the <A HREF='http://desktop.google.com/privacypolicy.'''
+      '''html?hl=[LANG_CODE]'>Privacy Policy</A> and <A HREF='http://desktop'''
+      '''.google.com/privacyfaq.html?hl=[LANG_CODE]'>Privacy FAQ</A> online.''')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres ==
+                    'You can read the BEGIN_LINK_1Privacy PolicyEND_LINK_1 and '
+                    'BEGIN_LINK_2Privacy FAQEND_LINK_2 online.')
+
+    # Check that tags with linebreaks immediately preceding them are handled
+    # correctly.
+    msg = tr_html.HtmlToMessage(
+      '''You can read the
+<A HREF='http://desktop.google.com/privacypolicy.html?hl=[LANG_CODE]'>Privacy Policy</A>
+and <A HREF='http://desktop.google.com/privacyfaq.html?hl=[LANG_CODE]'>Privacy FAQ</A> online.''')
+    pres = msg.GetPresentableContent()
+    self.failUnless(pres == '''You can read the
+BEGIN_LINK_1Privacy PolicyEND_LINK_1
+and BEGIN_LINK_2Privacy FAQEND_LINK_2 online.''')
+
+    # Check that message-no-break comments are handled correctly.
+    msg = tr_html.HtmlToMessage('''Please <!-- message-no-break --><br /> don't break''')
+    pres = msg.GetPresentableContent()
+    self.failUnlessEqual(pres, '''Please BREAK don't break''')
+
+class TrHtmlUnittest(unittest.TestCase):
+  def testSetAttributes(self):
+    html = tr_html.TrHtml(StringIO(''))
+    self.failUnlessEqual(html.fold_whitespace_, False)
+    html.SetAttributes({})
+    self.failUnlessEqual(html.fold_whitespace_, False)
+    html.SetAttributes({'fold_whitespace': 'false'})
+    self.failUnlessEqual(html.fold_whitespace_, False)
+    html.SetAttributes({'fold_whitespace': 'true'})
+    self.failUnlessEqual(html.fold_whitespace_, True)
+
+  def testFoldWhitespace(self):
+    text = '<td>   Test     Message   </td>'
+
+    html = tr_html.TrHtml(StringIO(text))
+    html.Parse()
+    self.failUnlessEqual(html.skeleton_[1].GetMessage().GetPresentableContent(),
+                         'Test  Message')
+
+    html = tr_html.TrHtml(StringIO(text))
+    html.fold_whitespace_ = True
+    html.Parse()
+    self.failUnlessEqual(html.skeleton_[1].GetMessage().GetPresentableContent(),
+                         'Test Message')
+
+  def testTable(self):
+    html = tr_html.TrHtml(StringIO('''<table class="shaded-header"><tr>
+<td class="header-element b expand">Preferences</td>
+<td class="header-element s">
+<a href="http://desktop.google.com/preferences.html">Preferences Help</a>
+</td>
+</tr></table>'''))
+    html.Parse()
+    self.failUnless(html.skeleton_[3].GetMessage().GetPresentableContent() ==
+                    'BEGIN_LINKPreferences HelpEND_LINK')
+
+  def testSubmitAttribute(self):
+    html = tr_html.TrHtml(StringIO('''</td>
+<td class="header-element"><input type=submit value="Save Preferences"
+name=submit2></td>
+</tr></table>'''))
+    html.Parse()
+    self.failUnless(html.skeleton_[1].GetMessage().GetPresentableContent() ==
+                    'Save Preferences')
+
+  def testWhitespaceAfterInlineTag(self):
+    '''Test that even if there is whitespace after an inline tag at the start
+    of a translateable section the inline tag will be included.
+    '''
+    html = tr_html.TrHtml(
+        StringIO('''<label for=DISPLAYNONE><font size=-1> Hello</font>'''))
+    html.Parse()
+    self.failUnless(html.skeleton_[1].GetMessage().GetRealContent() ==
+                    '<font size=-1> Hello</font>')
+
+  def testSillyHeader(self):
+    html = tr_html.TrHtml(StringIO('''[!]
+title\tHello
+bingo
+bongo
+bla
+
+<p>Other stuff</p>'''))
+    html.Parse()
+    content = html.skeleton_[1].GetMessage().GetRealContent()
+    self.failUnless(content == 'Hello')
+    self.failUnless(html.skeleton_[-1] == '</p>')
+    # Right after the translateable the nontranslateable should start with
+    # a linebreak (this catches a bug we had).
+    self.failUnless(html.skeleton_[2][0] == '\n')
+
+
+  def testExplicitDescriptions(self):
+    html = tr_html.TrHtml(
+        StringIO('Hello [USER]<br/><!-- desc=explicit -->'
+                          '<input type="button">Go!</input>'))
+    html.Parse()
+    msg = html.GetCliques()[1].GetMessage()
+    self.failUnlessEqual(msg.GetDescription(), 'explicit')
+    self.failUnlessEqual(msg.GetRealContent(), 'Go!')
+
+    html = tr_html.TrHtml(
+        StringIO('Hello [USER]<br/><!-- desc=explicit\nmultiline -->'
+                          '<input type="button">Go!</input>'))
+    html.Parse()
+    msg = html.GetCliques()[1].GetMessage()
+    self.failUnlessEqual(msg.GetDescription(), 'explicit multiline')
+    self.failUnlessEqual(msg.GetRealContent(), 'Go!')
+
+
+  def testRegressionInToolbarAbout(self):
+    html = tr_html.TrHtml(util.PathFromRoot(r'grit/testdata/toolbar_about.html'))
+    html.Parse()
+    cliques = html.GetCliques()
+    for cl in cliques:
+      content = cl.GetMessage().GetRealContent()
+      if content.count('De parvis grandis acervus erit'):
+        self.failIf(content.count('$/translate'))
+
+
+  def HtmlFromFileWithManualCheck(self, f):
+    html = tr_html.TrHtml(f)
+    html.Parse()
+
+    # For manual results inspection only...
+    list = []
+    for item in html.skeleton_:
+      if isinstance(item, six.string_types):
+        list.append(item)
+      else:
+        list.append(item.GetMessage().GetPresentableContent())
+
+    return html
+
+
+  def testPrivacyHtml(self):
+    html = self.HtmlFromFileWithManualCheck(
+      util.PathFromRoot(r'grit/testdata/privacy.html'))
+
+    self.failUnless(html.skeleton_[1].GetMessage().GetRealContent() ==
+                    'Privacy and Google Desktop Search')
+    self.failUnless(html.skeleton_[3].startswith('<'))
+    self.failUnless(len(html.skeleton_) > 10)
+
+
+  def testPreferencesHtml(self):
+    html = self.HtmlFromFileWithManualCheck(
+      util.PathFromRoot(r'grit/testdata/preferences.html'))
+
+    # Verify that we don't get '[STATUS-MESSAGE]' as the original content of
+    # one of the MessageClique objects (it would be a placeholder-only message
+    # and we're supposed to have stripped those).
+
+    for item in [x for x in html.skeleton_
+                 if isinstance(x, clique.MessageClique)]:
+      if (item.GetMessage().GetRealContent() == '[STATUS-MESSAGE]' or
+          item.GetMessage().GetRealContent() == '[ADDIN-DO] [ADDIN-OPTIONS]'):
+        self.fail()
+
+    self.failUnless(len(html.skeleton_) > 100)
+
+  def AssertNumberOfTranslateables(self, files, num):
+    '''Fails if any of the files in files don't have exactly
+    num translateable sections.
+
+    Args:
+      files: ['file1', 'file2']
+      num: 3
+    '''
+    for f in files:
+      f = util.PathFromRoot(r'grit/testdata/%s' % f)
+      html = self.HtmlFromFileWithManualCheck(f)
+      self.failUnless(len(html.GetCliques()) == num)
+
+  def testFewTranslateables(self):
+    self.AssertNumberOfTranslateables(['browser.html', 'email_thread.html',
+                                       'header.html', 'mini.html',
+                                       'oneclick.html', 'script.html',
+                                       'time_related.html', 'versions.html'], 0)
+    self.AssertNumberOfTranslateables(['footer.html', 'hover.html'], 1)
+
+  def testOtherHtmlFilesForManualInspection(self):
+    files = [
+      'about.html', 'bad_browser.html', 'cache_prefix.html',
+      'cache_prefix_file.html', 'chat_result.html', 'del_footer.html',
+      'del_header.html', 'deleted.html', 'details.html', 'email_result.html',
+      'error.html', 'explicit_web.html', 'footer.html',
+      'homepage.html', 'indexing_speed.html',
+      'install_prefs.html', 'install_prefs2.html',
+      'oem_enable.html', 'oem_non_admin.html', 'onebox.html',
+      'password.html', 'quit_apps.html', 'recrawl.html',
+      'searchbox.html', 'sidebar_h.html', 'sidebar_v.html', 'status.html',
+    ]
+    for f in files:
+      self.HtmlFromFileWithManualCheck(
+        util.PathFromRoot(r'grit/testdata/%s' % f))
+
+  def testTranslate(self):
+    # Note that the English translation of documents that use character
+    # literals (e.g. ©) will not be the same as the original document
+    # because the character literal will be transformed into the Unicode
+    # character itself.  So for this test we choose some relatively complex
+    # HTML without character entities (but with   because that's handled
+    # specially).
+    html = tr_html.TrHtml(StringIO('''  <script>
+      <!--
+      function checkOffice() { var w = document.getElementById("h7");
+      var e = document.getElementById("h8"); var o = document.getElementById("h10");
+      if (!(w.checked || e.checked)) { o.checked=0;o.disabled=1;} else {o.disabled=0;} }
+      // -->
+        </script>
+        <input type=checkbox [CHECK-DOC] name=DOC id=h7 onclick='checkOffice()'>
+        <label for=h7> Word</label><br>
+        <input type=checkbox [CHECK-XLS] name=XLS id=h8 onclick='checkOffice()'>
+        <label for=h8> Excel</label><br>
+        <input type=checkbox [CHECK-PPT] name=PPT id=h9>
+        <label for=h9> PowerPoint</label><br>
+        </span></td><td nowrap valign=top><span class="s">
+        <input type=checkbox [CHECK-PDF] name=PDF id=hpdf>
+        <label for=hpdf> PDF</label><br>
+        <input type=checkbox [CHECK-TXT] name=TXT id=h6>
+        <label for=h6> Text, media, and other files</label><br>
+       </tr>  
+       <tr><td nowrap valign=top colspan=3><span class="s"><br />
+       <input type=checkbox [CHECK-SECUREOFFICE] name=SECUREOFFICE id=h10>
+        <label for=h10> Password-protected Office documents (Word, Excel)</label><br />
+        <input type=checkbox [DISABLED-HTTPS] [CHECK-HTTPS] name=HTTPS id=h12><label
+        for=h12> Secure pages (HTTPS) in web history</label></span></td></tr>
+      </table>'''))
+    html.Parse()
+    trans = html.Translate('en')
+    if (html.GetText() != trans):
+      self.fail()
+
+
+  def testHtmlToMessageWithBlockTags(self):
+    msg = tr_html.HtmlToMessage(
+      'Hello<p>Howdie<img alt="bingo" src="image.gif">', True)
+    result = msg.GetPresentableContent()
+    self.failUnless(
+      result == 'HelloBEGIN_PARAGRAPHHowdieBEGIN_BLOCKbingoEND_BLOCK')
+
+    msg = tr_html.HtmlToMessage(
+      'Hello<p>Howdie<input type="button" value="bingo">', True)
+    result = msg.GetPresentableContent()
+    self.failUnless(
+      result == 'HelloBEGIN_PARAGRAPHHowdieBEGIN_BLOCKbingoEND_BLOCK')
+
+
+  def testHtmlToMessageRegressions(self):
+    msg = tr_html.HtmlToMessage(' - ', True)
+    result = msg.GetPresentableContent()
+    self.failUnless(result == ' - ')
+
+
+  def testEscapeUnescaped(self):
+    text = '©  & "<hello>"'
+    unescaped = util.UnescapeHtml(text)
+    self.failUnless(unescaped == u'\u00a9\u00a0 & "<hello>"')
+    escaped_unescaped = util.EscapeHtml(unescaped, True)
+    self.failUnless(escaped_unescaped ==
+                    u'\u00a9\u00a0 & "<hello>"')
+
+  def testRegressionCjkHtmlFile(self):
+    # TODO(joi) Fix this problem where unquoted attributes that
+    # have a value that is CJK characters causes the regular expression
+    # match never to return.  (culprit is the _ELEMENT regexp(
+    if False:
+      html = self.HtmlFromFileWithManualCheck(util.PathFromRoot(
+        r'grit/testdata/ko_oem_enable_bug.html'))
+      self.failUnless(True)
+
+  def testRegressionCpuHang(self):
+    # If this regression occurs, the unit test will never return
+    html = tr_html.TrHtml(StringIO(
+      '''<input type=text size=12 id=advFileTypeEntry [~SHOW-FILETYPE-BOX~] value="[EXT]" name=ext>'''))
+    html.Parse()
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/gather/txt.py b/tools/grit/grit/gather/txt.py
new file mode 100644
index 0000000000..e5c10abc28
--- /dev/null
+++ b/tools/grit/grit/gather/txt.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Supports making amessage from a text file.
+'''
+
+from __future__ import print_function
+
+from grit.gather import interface
+from grit import tclib
+
+
+class TxtFile(interface.GathererBase):
+  '''A text file gatherer.  Very simple, all text from the file becomes a
+  single clique.
+  '''
+
+  def Parse(self):
+    self.text_ = self._LoadInputFile()
+    self.clique_ = self.uberclique.MakeClique(tclib.Message(text=self.text_))
+
+  def GetText(self):
+    '''Returns the text of what is being gathered.'''
+    return self.text_
+
+  def GetTextualIds(self):
+    return [self.extkey]
+
+  def GetCliques(self):
+    '''Returns the MessageClique objects for all translateable portions.'''
+    return [self.clique_]
+
+  def Translate(self, lang, pseudo_if_not_available=True,
+                skeleton_gatherer=None, fallback_to_english=False):
+    return self.clique_.MessageForLanguage(lang,
+                                           pseudo_if_not_available,
+                                           fallback_to_english).GetRealContent()
diff --git a/tools/grit/grit/gather/txt_unittest.py b/tools/grit/grit/gather/txt_unittest.py
new file mode 100644
index 0000000000..abb9ed98d7
--- /dev/null
+++ b/tools/grit/grit/gather/txt_unittest.py
@@ -0,0 +1,35 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for TxtFile gatherer'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+
+import unittest
+
+from six import StringIO
+
+from grit.gather import txt
+
+
+class TxtUnittest(unittest.TestCase):
+  def testGather(self):
+    input = StringIO('Hello there\nHow are you?')
+    gatherer = txt.TxtFile(input)
+    gatherer.Parse()
+    self.failUnless(gatherer.GetText() == input.getvalue())
+    self.failUnless(len(gatherer.GetCliques()) == 1)
+    self.failUnless(gatherer.GetCliques()[0].GetMessage().GetRealContent() ==
+                    input.getvalue())
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/grd_reader.py b/tools/grit/grit/grd_reader.py
new file mode 100644
index 0000000000..b7bb782977
--- /dev/null
+++ b/tools/grit/grit/grd_reader.py
@@ -0,0 +1,238 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Class for reading GRD files into memory, without processing them.
+'''
+
+from __future__ import print_function
+
+import os.path
+import sys
+import xml.sax
+import xml.sax.handler
+
+import six
+
+from grit import exception
+from grit import util
+from grit.node import mapping
+from grit.node import misc
+
+
+class StopParsingException(Exception):
+  '''An exception used to stop parsing.'''
+  pass
+
+
+class GrdContentHandler(xml.sax.handler.ContentHandler):
+  def __init__(self, stop_after, debug, dir, defines, tags_to_ignore,
+               target_platform, source):
+    # Invariant of data:
+    # 'root' is the root of the parse tree being created, or None if we haven't
+    # parsed out any elements.
+    # 'stack' is the a stack of elements that we push new nodes onto and
+    # pop from when they finish parsing, or [] if we are not currently parsing.
+    # 'stack[-1]' is the top of the stack.
+    self.root = None
+    self.stack = []
+    self.stop_after = stop_after
+    self.debug = debug
+    self.dir = dir
+    self.defines = defines
+    self.tags_to_ignore = tags_to_ignore or set()
+    self.ignore_depth = 0
+    self.target_platform = target_platform
+    self.source = source
+
+  def startElement(self, name, attrs):
+    if self.ignore_depth or name in self.tags_to_ignore:
+      if self.debug and self.ignore_depth == 0:
+        print("Ignoring element %s and its children" % name)
+      self.ignore_depth += 1
+      return
+
+    if self.debug:
+      attr_list = ' '.join('%s="%s"' % kv for kv in attrs.items())
+      print("Starting parsing of element %s with attributes %r" %
+            (name, attr_list or '(none)'))
+
+    typeattr = attrs.get('type')
+    node = mapping.ElementToClass(name, typeattr)()
+    node.source = self.source
+
+    if self.stack:
+      self.stack[-1].AddChild(node)
+      node.StartParsing(name, self.stack[-1])
+    else:
+      assert self.root is None
+      self.root = node
+      if isinstance(self.root, misc.GritNode):
+        if self.target_platform:
+          self.root.SetTargetPlatform(self.target_platform)
+      node.StartParsing(name, None)
+      if self.defines:
+        node.SetDefines(self.defines)
+    self.stack.append(node)
+
+    for attr, attrval in attrs.items():
+      node.HandleAttribute(attr, attrval)
+
+  def endElement(self, name):
+    if self.ignore_depth:
+      self.ignore_depth -= 1
+      return
+
+    if name == 'part':
+      partnode = self.stack[-1]
+      partnode.started_inclusion = True
+      # Add the contents of the sub-grd file as children of the <part> node.
+      partname = os.path.join(self.dir, partnode.GetInputPath())
+      # Check the GRDP file exists.
+      if not os.path.exists(partname):
+        raise exception.FileNotFound(partname)
+      # Exceptions propagate to the handler in grd_reader.Parse().
+      oldsource = self.source
+      try:
+        self.source = partname
+        xml.sax.parse(partname, GrdPartContentHandler(self))
+      finally:
+        self.source = oldsource
+
+    if self.debug:
+      print("End parsing of element %s" % name)
+    self.stack.pop().EndParsing()
+
+    if name == self.stop_after:
+      raise StopParsingException()
+
+  def characters(self, content):
+    if self.ignore_depth == 0:
+      if self.stack[-1]:
+        self.stack[-1].AppendContent(content)
+
+  def ignorableWhitespace(self, whitespace):
+    # TODO(joi): This is not supported by expat. Should use a different XML
+    # parser?
+    pass
+
+
+class GrdPartContentHandler(xml.sax.handler.ContentHandler):
+  def __init__(self, parent):
+    self.parent = parent
+    self.depth = 0
+
+  def startElement(self, name, attrs):
+    if self.depth:
+      self.parent.startElement(name, attrs)
+    else:
+      if name != 'grit-part':
+        raise exception.MissingElement("root tag must be <grit-part>")
+      if attrs:
+        raise exception.UnexpectedAttribute(
+            "<grit-part> tag must not have attributes")
+    self.depth += 1
+
+  def endElement(self, name):
+    self.depth -= 1
+    if self.depth:
+      self.parent.endElement(name)
+
+  def characters(self, content):
+    self.parent.characters(content)
+
+  def ignorableWhitespace(self, whitespace):
+    self.parent.ignorableWhitespace(whitespace)
+
+
+def Parse(filename_or_stream, dir=None, stop_after=None, first_ids_file=None,
+          debug=False, defines=None, tags_to_ignore=None, target_platform=None,
+          predetermined_ids_file=None):
+  '''Parses a GRD file into a tree of nodes (from grit.node).
+
+  If filename_or_stream is a stream, 'dir' should point to the directory
+  notionally containing the stream (this feature is only used in unit tests).
+
+  If 'stop_after' is provided, the parsing will stop once the first node
+  with this name has been fully parsed (including all its contents).
+
+  If 'debug' is true, lots of information about the parsing events will be
+  printed out during parsing of the file.
+
+  If 'first_ids_file' is non-empty, it is used to override the setting for the
+  first_ids_file attribute of the <grit> root node. Note that the first_ids_file
+  parameter should be relative to the cwd, even though the first_ids_file
+  attribute of the <grit> node is relative to the grd file.
+
+  If 'target_platform' is set, this is used to determine the target
+  platform of builds, instead of using |sys.platform|.
+
+  Args:
+    filename_or_stream: './bla.xml'
+    dir: None (if filename_or_stream is a filename) or '.'
+    stop_after: 'inputs'
+    first_ids_file: 'GRIT_DIR/../gritsettings/resource_ids'
+    debug: False
+    defines: dictionary of defines, like {'chromeos': '1'}
+    target_platform: None or the value that would be returned by sys.platform
+        on your target platform.
+    predetermined_ids_file: File path to a file containing a pre-determined
+        mapping from resource names to resource ids which will be used to assign
+        resource ids to those resources.
+
+  Return:
+    Subclass of grit.node.base.Node
+
+  Throws:
+    grit.exception.Parsing
+  '''
+
+  if isinstance(filename_or_stream, six.string_types):
+    source = filename_or_stream
+    if dir is None:
+      dir = util.dirname(filename_or_stream)
+  else:
+    source = None
+
+  handler = GrdContentHandler(stop_after=stop_after, debug=debug, dir=dir,
+                              defines=defines, tags_to_ignore=tags_to_ignore,
+                              target_platform=target_platform, source=source)
+  try:
+    xml.sax.parse(filename_or_stream, handler)
+  except StopParsingException:
+    assert stop_after
+    pass
+  except:
+    if not debug:
+      print("parse exception: run GRIT with the -x flag to debug .grd problems")
+    raise
+
+  if handler.root.name != 'grit':
+    raise exception.MissingElement("root tag must be <grit>")
+
+  if hasattr(handler.root, 'SetOwnDir'):
+    # Fix up the base_dir so it is relative to the input file.
+    assert dir is not None
+    handler.root.SetOwnDir(dir)
+
+  if isinstance(handler.root, misc.GritNode):
+    handler.root.SetPredeterminedIdsFile(predetermined_ids_file)
+    if first_ids_file:
+      # Make the path to the first_ids_file relative to the grd file,
+      # unless it begins with GRIT_DIR.
+      GRIT_DIR_PREFIX = 'GRIT_DIR'
+      if not (first_ids_file.startswith(GRIT_DIR_PREFIX)
+          and first_ids_file[len(GRIT_DIR_PREFIX)] in ['/', '\\']):
+        rel_dir = os.path.relpath(os.getcwd(), dir)
+        first_ids_file = util.normpath(os.path.join(rel_dir, first_ids_file))
+      handler.root.attrs['first_ids_file'] = first_ids_file
+    # Assign first ids to the nodes that don't have them.
+    handler.root.AssignFirstIds(filename_or_stream, defines)
+
+  return handler.root
+
+
+if __name__ == '__main__':
+  util.ChangeStdoutEncoding()
+  print(six.text_type(Parse(sys.argv[1])))
diff --git a/tools/grit/grit/grd_reader_unittest.py b/tools/grit/grit/grd_reader_unittest.py
new file mode 100644
index 0000000000..920a92f9c0
--- /dev/null
+++ b/tools/grit/grit/grd_reader_unittest.py
@@ -0,0 +1,346 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grd_reader package'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import unittest
+
+import six
+from six import StringIO
+
+from grit import exception
+from grit import grd_reader
+from grit import util
+from grit.node import empty
+from grit.node import message
+
+
+class GrdReaderUnittest(unittest.TestCase):
+  def testParsingAndXmlOutput(self):
+    input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit base_dir="." current_release="3" latest_public_release="2" source_lang_id="en-US">
+  <release seq="3">
+    <includes>
+      <include file="images/logo.gif" name="ID_LOGO" type="gif" />
+    </includes>
+    <messages>
+      <if expr="True">
+        <message desc="Printed to greet the currently logged in user" name="IDS_GREETING">
+          Hello <ph name="USERNAME">%s<ex>Joi</ex></ph>, how are you doing today?
+        </message>
+      </if>
+    </messages>
+    <structures>
+      <structure file="rc_files/dialogs.rc" name="IDD_NARROW_DIALOG" type="dialog">
+        <skeleton expr="lang == 'fr-FR'" file="bla.rc" variant_of_revision="3" />
+      </structure>
+      <structure file="rc_files/version.rc" name="VS_VERSION_INFO" type="version" />
+    </structures>
+  </release>
+  <translations>
+    <file lang="nl" path="nl_translations.xtb" />
+  </translations>
+  <outputs>
+    <output filename="resource.h" type="rc_header" />
+    <output filename="resource.rc" lang="en-US" type="rc_all" />
+  </outputs>
+</grit>'''
+    pseudo_file = StringIO(input)
+    tree = grd_reader.Parse(pseudo_file, '.')
+    output = six.text_type(tree)
+    expected_output = input.replace(u' base_dir="."', u'')
+    self.assertEqual(expected_output, output)
+    self.failUnless(tree.GetNodeById('IDS_GREETING'))
+
+
+  def testStopAfter(self):
+    input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+  <outputs>
+    <output filename="resource.h" type="rc_header" />
+    <output filename="resource.rc" lang="en-US" type="rc_all" />
+  </outputs>
+  <release seq="3">
+    <includes>
+      <include type="gif" name="ID_LOGO" file="images/logo.gif"/>
+    </includes>
+  </release>
+</grit>'''
+    pseudo_file = StringIO(input)
+    tree = grd_reader.Parse(pseudo_file, '.', stop_after='outputs')
+    # only an <outputs> child
+    self.failUnless(len(tree.children) == 1)
+    self.failUnless(tree.children[0].name == 'outputs')
+
+  def testLongLinesWithComments(self):
+    input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+  <release seq="3">
+    <messages>
+      <message name="IDS_GREETING" desc="Printed to greet the currently logged in user">
+        This is a very long line with no linebreaks yes yes it stretches on <!--
+        -->and on <!--
+        -->and on!
+      </message>
+    </messages>
+  </release>
+</grit>'''
+    pseudo_file = StringIO(input)
+    tree = grd_reader.Parse(pseudo_file, '.')
+
+    greeting = tree.GetNodeById('IDS_GREETING')
+    self.failUnless(greeting.GetCliques()[0].GetMessage().GetRealContent() ==
+                    'This is a very long line with no linebreaks yes yes it '
+                    'stretches on and on and on!')
+
+  def doTestAssignFirstIds(self, first_ids_path):
+    input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3"
+      base_dir="." first_ids_file="%s">
+  <release seq="3">
+    <messages>
+      <message name="IDS_TEST" desc="test">
+        test
+      </message>
+    </messages>
+  </release>
+</grit>''' % first_ids_path
+    pseudo_file = StringIO(input)
+    grit_root_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
+                                 '..')
+    fake_input_path = os.path.join(
+        grit_root_dir, "grit/testdata/chrome/app/generated_resources.grd")
+    root = grd_reader.Parse(pseudo_file, os.path.split(fake_input_path)[0])
+    root.AssignFirstIds(fake_input_path, {})
+    messages_node = root.children[0].children[0]
+    self.failUnless(isinstance(messages_node, empty.MessagesNode))
+    self.failUnless(messages_node.attrs["first_id"] !=
+        empty.MessagesNode().DefaultAttributes()["first_id"])
+
+  def testAssignFirstIds(self):
+    self.doTestAssignFirstIds("../../tools/grit/resource_ids")
+
+  def testAssignFirstIdsUseGritDir(self):
+    self.doTestAssignFirstIds("GRIT_DIR/grit/testdata/tools/grit/resource_ids")
+
+  def testAssignFirstIdsMultipleMessages(self):
+    """If there are multiple messages sections, the resource_ids file
+    needs to list multiple first_id values."""
+    input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3"
+      base_dir="." first_ids_file="resource_ids">
+  <release seq="3">
+    <messages>
+      <message name="IDS_TEST" desc="test">
+        test
+      </message>
+    </messages>
+    <messages>
+      <message name="IDS_TEST2" desc="test">
+        test2
+      </message>
+    </messages>
+  </release>
+</grit>'''
+    pseudo_file = StringIO(input)
+    grit_root_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
+                                 '..')
+    fake_input_path = os.path.join(grit_root_dir, "grit/testdata/test.grd")
+
+    root = grd_reader.Parse(pseudo_file, os.path.split(fake_input_path)[0])
+    root.AssignFirstIds(fake_input_path, {})
+    messages_node = root.children[0].children[0]
+    self.assertTrue(isinstance(messages_node, empty.MessagesNode))
+    self.assertEqual('100', messages_node.attrs["first_id"])
+    messages_node = root.children[0].children[1]
+    self.assertTrue(isinstance(messages_node, empty.MessagesNode))
+    self.assertEqual('10000', messages_node.attrs["first_id"])
+
+  def testUseNameForIdAndPpIfdef(self):
+    input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+  <release seq="3">
+    <messages>
+      <if expr="pp_ifdef('hello')">
+        <message name="IDS_HELLO" use_name_for_id="true">
+          Hello!
+        </message>
+      </if>
+    </messages>
+  </release>
+</grit>'''
+    pseudo_file = StringIO(input)
+    root = grd_reader.Parse(pseudo_file, '.', defines={'hello': '1'})
+
+    # Check if the ID is set to the name. In the past, there was a bug
+    # that caused the ID to be a generated number.
+    hello = root.GetNodeById('IDS_HELLO')
+    self.failUnless(hello.GetCliques()[0].GetId() == 'IDS_HELLO')
+
+  def testUseNameForIdWithIfElse(self):
+    input = u'''<?xml version="1.0" encoding="UTF-8"?>
+<grit latest_public_release="2" source_lang_id="en-US" current_release="3" base_dir=".">
+  <release seq="3">
+    <messages>
+      <if expr="pp_ifdef('hello')">
+        <then>
+          <message name="IDS_HELLO" use_name_for_id="true">
+            Hello!
+          </message>
+        </then>
+        <else>
+          <message name="IDS_HELLO" use_name_for_id="true">
+            Yellow!
+          </message>
+        </else>
+      </if>
+    </messages>
+  </release>
+</grit>'''
+    pseudo_file = StringIO(input)
+    root = grd_reader.Parse(pseudo_file, '.', defines={'hello': '1'})
+
+    # Check if the ID is set to the name. In the past, there was a bug
+    # that caused the ID to be a generated number.
+    hello = root.GetNodeById('IDS_HELLO')
+    self.failUnless(hello.GetCliques()[0].GetId() == 'IDS_HELLO')
+
+  def testPartInclusionAndCorrectSource(self):
+    arbitrary_path_grd = u'''\
+        <grit-part>
+          <message name="IDS_TEST5" desc="test5">test5</message>
+        </grit-part>'''
+    tmp_dir = util.TempDir({'arbitrary_path.grp': arbitrary_path_grd})
+    arbitrary_path_grd_file = tmp_dir.GetPath('arbitrary_path.grp')
+    top_grd = u'''\
+        <grit latest_public_release="2" current_release="3">
+          <release seq="3">
+            <messages>
+              <message name="IDS_TEST" desc="test">
+                test
+              </message>
+              <part file="sub.grp" />
+              <part file="%s" />
+            </messages>
+          </release>
+        </grit>''' % arbitrary_path_grd_file
+    sub_grd = u'''\
+        <grit-part>
+          <message name="IDS_TEST2" desc="test2">test2</message>
+          <part file="subsub.grp" />
+          <message name="IDS_TEST3" desc="test3">test3</message>
+        </grit-part>'''
+    subsub_grd = u'''\
+        <grit-part>
+          <message name="IDS_TEST4" desc="test4">test4</message>
+        </grit-part>'''
+    expected_output = u'''\
+        <grit current_release="3" latest_public_release="2">
+          <release seq="3">
+            <messages>
+              <message desc="test" name="IDS_TEST">
+                test
+              </message>
+              <part file="sub.grp">
+                <message desc="test2" name="IDS_TEST2">
+                  test2
+                </message>
+                <part file="subsub.grp">
+                  <message desc="test4" name="IDS_TEST4">
+                    test4
+                  </message>
+                </part>
+                <message desc="test3" name="IDS_TEST3">
+                  test3
+                </message>
+              </part>
+              <part file="%s">
+                <message desc="test5" name="IDS_TEST5">
+                  test5
+                </message>
+              </part>
+            </messages>
+          </release>
+        </grit>''' % arbitrary_path_grd_file
+
+    with util.TempDir({'sub.grp': sub_grd,
+                       'subsub.grp': subsub_grd}) as tmp_sub_dir:
+      output = grd_reader.Parse(StringIO(top_grd),
+                                tmp_sub_dir.GetPath())
+      correct_sources = {
+        'IDS_TEST': None,
+        'IDS_TEST2': tmp_sub_dir.GetPath('sub.grp'),
+        'IDS_TEST3': tmp_sub_dir.GetPath('sub.grp'),
+        'IDS_TEST4': tmp_sub_dir.GetPath('subsub.grp'),
+        'IDS_TEST5': arbitrary_path_grd_file,
+      }
+
+    for node in output.ActiveDescendants():
+      with node:
+        if isinstance(node, message.MessageNode):
+          self.assertEqual(correct_sources[node.attrs.get('name')], node.source)
+    self.assertEqual(expected_output.split(), output.FormatXml().split())
+    tmp_dir.CleanUp()
+
+  def testPartInclusionFailure(self):
+    template = u'''
+      <grit latest_public_release="2" current_release="3">
+        <outputs>
+          %s
+        </outputs>
+      </grit>'''
+
+    part_failures = [
+        (exception.UnexpectedContent, u'<part file="x">fnord</part>'),
+        (exception.UnexpectedChild,
+         u'<part file="x"><output filename="x" type="y" /></part>'),
+        (exception.FileNotFound, u'<part file="yet_created_x" />'),
+    ]
+    for raises, data in part_failures:
+      data = StringIO(template % data)
+      self.assertRaises(raises, grd_reader.Parse, data, '.')
+
+    gritpart_failures = [
+        (exception.UnexpectedAttribute, u'<grit-part file="xyz"></grit-part>'),
+        (exception.MissingElement, u'<output filename="x" type="y" />'),
+    ]
+    for raises, data in gritpart_failures:
+      top_grd = StringIO(template % u'<part file="bad.grp" />')
+      with util.TempDir({'bad.grp': data}) as temp_dir:
+        self.assertRaises(raises, grd_reader.Parse, top_grd, temp_dir.GetPath())
+
+  def testEarlyEnoughPlatformSpecification(self):
+    # This is a regression test for issue
+    # https://code.google.com/p/grit-i18n/issues/detail?id=23
+    grd_text = u'''<?xml version="1.0" encoding="UTF-8"?>
+      <grit latest_public_release="1" current_release="1">
+        <release seq="1">
+          <messages>
+            <if expr="not pp_ifdef('use_titlecase')">
+              <message name="IDS_XYZ">foo</message>
+            </if>
+            <!-- The assumption is that use_titlecase is never true for
+                 this platform. When the platform isn't set to 'android'
+                 early enough, we get a duplicate message name. -->
+            <if expr="os == '%s'">
+              <message name="IDS_XYZ">boo</message>
+            </if>
+          </messages>
+        </release>
+      </grit>''' % sys.platform
+    with util.TempDir({}) as temp_dir:
+      grd_reader.Parse(StringIO(grd_text), temp_dir.GetPath(),
+                       target_platform='android')
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/grit-todo.xml b/tools/grit/grit/grit-todo.xml
new file mode 100644
index 0000000000..b8c20fdfad
--- /dev/null
+++ b/tools/grit/grit/grit-todo.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="windows-1252"?>
+<TODOLIST FILEFORMAT="6" PROJECTNAME="GRIT" NEXTUNIQUEID="56" FILEVERSION="69" LASTMODIFIED="2005-08-19">
+    <TASK STARTDATESTRING="2005-04-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38453.49975694" TITLE="check 'name' attribute is unique" TIMEESTUNITS="H" ID="2" PERCENTDONE="100" STARTDATE="38450.00000000" DONEDATESTRING="2005-04-11" POS="22" DONEDATE="38453.00000000"/>
+    <TASK STARTDATESTRING="2005-04-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38488.48189815" TITLE="import id-calculating code" TIMEESTUNITS="H" ID="3" PERCENTDONE="100" STARTDATE="38450.00000000" DONEDATESTRING="2005-05-16" POS="13" DONEDATE="38488.00000000"/>
+    <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38488.48209491" TITLE="Import tool for existing translations" TIMEESTUNITS="H" ID="6" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="12" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00805556" TITLE="Export XMBs" TIMEESTUNITS="H" ID="8" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-08" POS="20" DONEDATE="38511.00000000"/>
+    <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00924769" TITLE="Initial Integration" TIMEESTUNITS="H" ID="10" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-08" POS="10" DONEDATE="38511.00000000">
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38496.54048611" TITLE="parser for %s strings" TIMEESTUNITS="H" ID="4" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-24" POS="2" DONEDATE="38496.00000000"/>
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38497.00261574" TITLE="import tool for existing RC files" TIMEESTUNITS="H" ID="5" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-25" POS="4" DONEDATE="38497.00000000">
+            <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38496.92990741" TITLE="handle button value= and img alt= in message HTML text" TIMEESTUNITS="H" ID="22" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-05-24" POS="1" DONEDATE="38496.00000000"/>
+            <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38497.00258102" TITLE="&nbsp; bug" TIMEESTUNITS="H" ID="23" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-05-25" POS="2" DONEDATE="38497.00000000"/>
+        </TASK>
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38490.61171296" TITLE="grit build" TIMEESTUNITS="H" ID="7" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-18" POS="6" DONEDATE="38490.00000000">
+            <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38490.61168981" TITLE="use IDs gathered from gatherers for .h file" TIMEESTUNITS="H" ID="20" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-18" POS="1" DONEDATE="38490.00000000"/>
+        </TASK>
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38504.55199074" TITLE="SCons Integration" TIMEESTUNITS="H" ID="9" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-01" POS="1" DONEDATE="38504.00000000"/>
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38490.61181713" TITLE="handle includes" TIMEESTUNITS="H" ID="12" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-05-18" POS="5" DONEDATE="38490.00000000"/>
+        <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38507.98567130" TITLE="output translated HTML templates" TIMEESTUNITS="H" ID="25" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-04" POS="3" DONEDATE="38507.00000000"/>
+        <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38507.99394676" TITLE="bug: re-escape too much in RC dialogs etc." TIMEESTUNITS="H" ID="38" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-04" POS="7" DONEDATE="38507.00000000"/>
+    </TASK>
+    <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46444444" TITLE="handle structure variants" TIMEESTUNITS="H" ID="11" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="15" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46456019" TITLE="handle include variants" TIMEESTUNITS="H" ID="13" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="17" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46537037" TITLE="handle translateable text for includes (e.g. image text)" TIMEESTUNITS="H" ID="14" PERCENTDONE="100" STARTDATE="38488.00000000" DONEDATESTRING="2005-06-16" POS="14" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46712963" TITLE="ddoc" TIMEESTUNITS="H" ID="15" STARTDATE="38488.00000000" POS="4">
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46718750" TITLE="review comments miket" TIMEESTUNITS="H" ID="16" STARTDATE="38488.00000000" POS="2"/>
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46722222" TITLE="review comments pdoyle" TIMEESTUNITS="H" ID="17" STARTDATE="38488.00000000" POS="1"/>
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.46732639" TITLE="remove 'extkey' from structure" TIMEESTUNITS="H" ID="18" STARTDATE="38488.00000000" POS="3"/>
+        <TASK STARTDATESTRING="2005-05-16" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38489.53537037" TITLE="add 'encoding' to structure" TIMEESTUNITS="H" ID="19" STARTDATE="38488.00000000" POS="6"/>
+        <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38503.55304398" TITLE="document limitation: emitter doesn't emit the translated HTML templates" TIMEESTUNITS="H" ID="30" STARTDATE="38503.00000000" POS="4"/>
+        <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38504.58541667" TITLE="add 'internal_comment' to <message>" TIMEESTUNITS="H" ID="32" STARTDATE="38503.00000000" POS="5"/>
+        <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.73391204" TITLE="<outputs> can not have paths (because of SCons integration - goes to build dir)" TIMEESTUNITS="H" ID="36" STARTDATE="38503.00000000" POS="9"/>
+        <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38506.64265046" TITLE="<identifers> and <identifier> nodes" TIMEESTUNITS="H" ID="37" STARTDATE="38503.00000000" POS="10"/>
+        <TASK STARTDATESTRING="2005-06-23" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38526.62344907" TITLE="<structure> can have 'exclude_from_rc' attribute (default false)" TIMEESTUNITS="H" ID="47" STARTDATE="38526.00000000" POS="8"/>
+        <TASK STARTDATESTRING="2005-06-23" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38531.94135417" TITLE="add 'enc_check' to <grit>" TIMEESTUNITS="H" ID="48" STARTDATE="38526.00000000" POS="7"/>
+    </TASK>
+    <TASK STARTDATESTRING="2005-05-18" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38492.51549769" TITLE="handle nontranslateable messages (in MessageClique?)" TIMEESTUNITS="H" ID="21" PERCENTDONE="100" STARTDATE="38490.00000000" DONEDATESTRING="2005-06-16" POS="16" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.70454861" TITLE="ask cprince about SCons builder in new mk system" TIMEESTUNITS="H" ID="24" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-02" POS="25" DONEDATE="38505.00000000"/>
+    <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38504.57436343" TITLE="fix AOL resource in trunk ("???????")" TIMEESTUNITS="H" ID="26" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-01" POS="19" DONEDATE="38504.00000000"/>
+    <TASK STARTDATESTRING="2005-05-24" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38498.53893519" TITLE="rc_all vs. rc_translateable vs. rc_nontranslateable" TIMEESTUNITS="H" ID="27" PERCENTDONE="100" STARTDATE="38496.00000000" DONEDATESTRING="2005-06-16" POS="6" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38509.45532407" TITLE="make separate .grb "outputs" file (and change SCons integ)  (??)" TIMEESTUNITS="H" ID="28" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-06" POS="8" DONEDATE="38509.00000000"/>
+    <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00939815" TITLE="fix unit tests so they run from any directory" TIMEESTUNITS="H" ID="33" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-08" POS="18" DONEDATE="38511.00000000"/>
+    <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38508.96640046" TITLE="Change R4 tool to CC correct team(s) on GRIT changes" TIMEESTUNITS="H" ID="39" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-05" POS="23" DONEDATE="38508.00000000"/>
+    <TASK STARTDATESTRING="2005-06-07" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00881944" TITLE="Document why wrapper.rc" TIMEESTUNITS="H" ID="40" PERCENTDONE="100" STARTDATE="38510.00000000" DONEDATESTRING="2005-06-08" POS="21" DONEDATE="38511.00000000"/>
+    <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00804398" TITLE="import XTBs" TIMEESTUNITS="H" ID="41" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-16" POS="11" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00875000" TITLE="Nightly build integration" TIMEESTUNITS="H" ID="42" STARTDATE="38511.00000000" POS="3"/>
+    <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00891204" TITLE="BUGS" TIMEESTUNITS="H" ID="43" STARTDATE="38511.00000000" POS="24">
+        <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38513.03375000" TITLE="Should report error if RC-section structure refers to does not exist" TIMEESTUNITS="H" ID="44" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-10" POS="1" DONEDATE="38513.00000000"/>
+    </TASK>
+    <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.00981481" TITLE="NEW FEATURES" TIMEESTUNITS="H" ID="45" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-16" POS="7" DONEDATE="38519.00000000">
+        <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.70077546" TITLE="Implement line-continuation feature (\ at end of line?)" TIMEESTUNITS="H" ID="34" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-16" POS="1" DONEDATE="38519.00000000"/>
+        <TASK STARTDATESTRING="2005-05-31" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38505.70262731" TITLE="Implement conditional inclusion & reflect the conditionals from R3 RC file" TIMEESTUNITS="H" ID="35" PERCENTDONE="100" STARTDATE="38503.00000000" DONEDATESTRING="2005-06-16" POS="2" DONEDATE="38519.00000000"/>
+    </TASK>
+    <TASK STARTDATESTRING="2005-06-08" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38511.01046296" TITLE="TC integration (one-way TO the TC)" TIMEESTUNITS="H" ID="46" PERCENTDONE="100" STARTDATE="38511.00000000" DONEDATESTRING="2005-06-16" POS="5" DONEDATE="38519.00000000"/>
+    <TASK STARTDATESTRING="2005-06-30" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38533.59072917" TITLE="bazaar20 ad for GRIT help" TIMEESTUNITS="H" ID="49" STARTDATE="38533.00000000" POS="2">
+        <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.72346065" TITLE="bazaar20 ideas" TIMEESTUNITS="H" ID="51" STARTDATE="38583.00000000" POS="1">
+            <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.72354167" TITLE="GUI for adding/editing messages" TIMEESTUNITS="H" ID="52" STARTDATE="38583.00000000" POS="2"/>
+            <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.72365741" TITLE="XLIFF import/export" TIMEESTUNITS="H" ID="54" STARTDATE="38583.00000000" POS="1"/>
+        </TASK>
+    </TASK>
+    <TASK STARTDATESTRING="2005-06-30" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.73721065" TITLE="internal_comment for all resource nodes (not just <message>)" TIMEESTUNITS="H" ID="50" PERCENTDONE="100" STARTDATE="38533.00000000" DONEDATESTRING="2005-08-19" POS="9" DONEDATE="38583.73721065"/>
+    <TASK STARTDATESTRING="2005-08-19" PRIORITY="5" TIMEESPENTUNITS="H" LASTMOD="38583.73743056" TITLE="Preserve XML comments - this gives us line continuation and more" TIMEESTUNITS="H" ID="55" STARTDATE="38583.72326389" POS="1"/>
+</TODOLIST>
diff --git a/tools/grit/grit/grit_runner.py b/tools/grit/grit/grit_runner.py
new file mode 100644
index 0000000000..26aa0d58c4
--- /dev/null
+++ b/tools/grit/grit/grit_runner.py
@@ -0,0 +1,334 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Command processor for GRIT.  This is the script you invoke to run the various
+GRIT tools.
+"""
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import getopt
+
+from grit import util
+
+import grit.extern.FP
+
+# Tool info factories; these import only within each factory to avoid
+# importing most of the GRIT code until required.
+def ToolFactoryBuild():
+  import grit.tool.build
+  return grit.tool.build.RcBuilder()
+
+def ToolFactoryBuildInfo():
+  import grit.tool.buildinfo
+  return grit.tool.buildinfo.DetermineBuildInfo()
+
+def ToolFactoryCount():
+  import grit.tool.count
+  return grit.tool.count.CountMessage()
+
+def ToolFactoryDiffStructures():
+  import grit.tool.diff_structures
+  return grit.tool.diff_structures.DiffStructures()
+
+def ToolFactoryMenuTranslationsFromParts():
+  import grit.tool.menu_from_parts
+  return grit.tool.menu_from_parts.MenuTranslationsFromParts()
+
+def ToolFactoryNewGrd():
+  import grit.tool.newgrd
+  return grit.tool.newgrd.NewGrd()
+
+def ToolFactoryResizeDialog():
+  import grit.tool.resize
+  return grit.tool.resize.ResizeDialog()
+
+def ToolFactoryRc2Grd():
+  import grit.tool.rc2grd
+  return grit.tool.rc2grd.Rc2Grd()
+
+def ToolFactoryTest():
+  import grit.tool.test
+  return grit.tool.test.TestTool()
+
+def ToolFactoryTranslationToTc():
+  import grit.tool.transl2tc
+  return grit.tool.transl2tc.TranslationToTc()
+
+def ToolFactoryUnit():
+  import grit.tool.unit
+  return grit.tool.unit.UnitTestTool()
+
+
+def ToolFactoryUpdateResourceIds():
+  import grit.tool.update_resource_ids
+  return grit.tool.update_resource_ids.UpdateResourceIds()
+
+
+def ToolFactoryXmb():
+  import grit.tool.xmb
+  return grit.tool.xmb.OutputXmb()
+
+def ToolAndroid2Grd():
+  import grit.tool.android2grd
+  return grit.tool.android2grd.Android2Grd()
+
+# Keys for the following map
+_FACTORY = 1
+_REQUIRES_INPUT = 2
+_HIDDEN = 3  # optional key - presence indicates tool is hidden
+
+# Maps tool names to the tool's module.  Done as a list of (key, value) tuples
+# instead of a map to preserve ordering.
+_TOOLS = [
+    ['android2grd', {
+        _FACTORY: ToolAndroid2Grd,
+        _REQUIRES_INPUT: False
+    }],
+    ['build', {
+        _FACTORY: ToolFactoryBuild,
+        _REQUIRES_INPUT: True
+    }],
+    ['buildinfo', {
+        _FACTORY: ToolFactoryBuildInfo,
+        _REQUIRES_INPUT: True
+    }],
+    ['count', {
+        _FACTORY: ToolFactoryCount,
+        _REQUIRES_INPUT: True
+    }],
+    [
+        'menufromparts',
+        {
+            _FACTORY: ToolFactoryMenuTranslationsFromParts,
+            _REQUIRES_INPUT: True,
+            _HIDDEN: True
+        }
+    ],
+    ['newgrd', {
+        _FACTORY: ToolFactoryNewGrd,
+        _REQUIRES_INPUT: False
+    }],
+    ['rc2grd', {
+        _FACTORY: ToolFactoryRc2Grd,
+        _REQUIRES_INPUT: False
+    }],
+    ['resize', {
+        _FACTORY: ToolFactoryResizeDialog,
+        _REQUIRES_INPUT: True
+    }],
+    ['sdiff', {
+        _FACTORY: ToolFactoryDiffStructures,
+        _REQUIRES_INPUT: False
+    }],
+    ['test', {
+        _FACTORY: ToolFactoryTest,
+        _REQUIRES_INPUT: True,
+        _HIDDEN: True
+    }],
+    [
+        'transl2tc',
+        {
+            _FACTORY: ToolFactoryTranslationToTc,
+            _REQUIRES_INPUT: False
+        }
+    ],
+    ['unit', {
+        _FACTORY: ToolFactoryUnit,
+        _REQUIRES_INPUT: False
+    }],
+    [
+        'update_resource_ids',
+        {
+            _FACTORY: ToolFactoryUpdateResourceIds,
+            _REQUIRES_INPUT: False
+        }
+    ],
+    ['xmb', {
+        _FACTORY: ToolFactoryXmb,
+        _REQUIRES_INPUT: True
+    }],
+]
+
+
+def PrintUsage():
+  tool_list = ''
+  for (tool, info) in _TOOLS:
+    if not _HIDDEN in info:
+      tool_list += '    %-12s %s\n' % (
+          tool, info[_FACTORY]().ShortDescription())
+
+  print("""GRIT - the Google Resource and Internationalization Tool
+
+Usage: grit [GLOBALOPTIONS] TOOL [args to tool]
+
+Global options:
+
+  -i INPUT  Specifies the INPUT file to use (a .grd file).  If this is not
+            specified, GRIT will look for the environment variable GRIT_INPUT.
+            If it is not present either, GRIT will try to find an input file
+            named 'resource.grd' in the current working directory.
+
+  -h MODULE Causes GRIT to use MODULE.UnsignedFingerPrint instead of
+            grit.extern.FP.UnsignedFingerprint.  MODULE must be
+            available somewhere in the PYTHONPATH search path.
+
+  -v        Print more verbose runtime information.
+
+  -x        Print extremely verbose runtime information.  Implies -v
+
+  -p FNAME  Specifies that GRIT should profile its execution and output the
+            results to the file FNAME.
+
+Tools:
+
+  TOOL can be one of the following:
+%s
+  For more information on how to use a particular tool, and the specific
+  arguments you can send to that tool, execute 'grit help TOOL'
+""" % (tool_list))
+
+
+class Options(object):
+  """Option storage and parsing."""
+
+  def __init__(self):
+    self.hash = None
+    self.input = None
+    self.verbose = False
+    self.extra_verbose = False
+    self.output_stream = sys.stdout
+    self.profile_dest = None
+
+  def ReadOptions(self, args):
+    """Reads options from the start of args and returns the remainder."""
+    (opts, args) = getopt.getopt(args, 'vxi:p:h:', ('help',))
+    for (key, val) in opts:
+      if key == '-h': self.hash = val
+      elif key == '-i': self.input = val
+      elif key == '-v':
+        self.verbose = True
+        util.verbose = True
+      elif key == '-x':
+        self.verbose = True
+        util.verbose = True
+        self.extra_verbose = True
+        util.extra_verbose = True
+      elif key == '-p': self.profile_dest = val
+      elif key == '--help':
+        PrintUsage()
+        sys.exit(0)
+
+    if not self.input:
+      if 'GRIT_INPUT' in os.environ:
+        self.input = os.environ['GRIT_INPUT']
+      else:
+        self.input = 'resource.grd'
+
+    return args
+
+  def __repr__(self):
+    return '(verbose: %d, input: %s)' % (
+        self.verbose, self.input)
+
+
+def _GetToolInfo(tool):
+  """Returns the info map for the tool named 'tool' or None if there is no
+  such tool."""
+  matches = [t for t in _TOOLS if t[0] == tool]
+  if not matches:
+    return None
+  else:
+    return matches[0][1]
+
+
+def Main(args=None):
+  """Parses arguments and does the appropriate thing."""
+  util.ChangeStdoutEncoding()
+
+  # Support for setuptools console wrappers.
+  if args is None:
+    args = sys.argv[1:]
+
+  options = Options()
+  try:
+    args = options.ReadOptions(args)  # args may be shorter after this
+  except getopt.GetoptError as e:
+    print("grit:", str(e))
+    print("Try running 'grit help' for valid options.")
+    return 1
+  if not args:
+    print("No tool provided.  Try running 'grit help' for a list of tools.")
+    return 2
+
+  tool = args[0]
+  if tool == 'help':
+    if len(args) == 1:
+      PrintUsage()
+      return 0
+    else:
+      tool = args[1]
+      if not _GetToolInfo(tool):
+        print("No such tool.  Try running 'grit help' for a list of tools.")
+        return 2
+
+      print("Help for 'grit %s' (for general help, run 'grit help'):\n" %
+            (tool,))
+      _GetToolInfo(tool)[_FACTORY]().ShowUsage()
+      return 0
+  if not _GetToolInfo(tool):
+    print("No such tool.  Try running 'grit help' for a list of tools.")
+    return 2
+
+  try:
+    if _GetToolInfo(tool)[_REQUIRES_INPUT]:
+      os.stat(options.input)
+  except OSError:
+    print('Input file %s not found.\n'
+          'To specify a different input file:\n'
+          '  1. Use the GRIT_INPUT environment variable.\n'
+          '  2. Use the -i command-line option.  This overrides '
+          'GRIT_INPUT.\n'
+          '  3. Specify neither GRIT_INPUT or -i and GRIT will try to load '
+          "'resource.grd'\n"
+          '     from the current directory.' % options.input)
+    return 2
+
+  if options.hash:
+    grit.extern.FP.UseUnsignedFingerPrintFromModule(options.hash)
+
+  try:
+    toolobject = _GetToolInfo(tool)[_FACTORY]()
+    if options.profile_dest:
+      import hotshot
+      prof = hotshot.Profile(options.profile_dest)
+      return prof.runcall(toolobject.Run, options, args[1:])
+    else:
+      return toolobject.Run(options, args[1:])
+  except getopt.GetoptError as e:
+    print("grit: %s: %s" % (tool, str(e)))
+    print("Try running 'grit help %s' for valid options." % (tool,))
+    return 1
+
+
+if __name__ == '__main__':
+  sys.path.append(
+      os.path.join(
+          os.path.dirname(
+              os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
+          'diagnosis'))
+  try:
+    import crbug_1001171
+    with crbug_1001171.DumpStateOnLookupError():
+      sys.exit(Main(sys.argv[1:]))
+  except ImportError:
+    pass
+
+  sys.exit(Main(sys.argv[1:]))
diff --git a/tools/grit/grit/grit_runner_unittest.py b/tools/grit/grit/grit_runner_unittest.py
new file mode 100644
index 0000000000..1487001d81
--- /dev/null
+++ b/tools/grit/grit/grit_runner_unittest.py
@@ -0,0 +1,42 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for grit.py'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import util
+import grit.grit_runner
+
+class OptionArgsUnittest(unittest.TestCase):
+  def setUp(self):
+    self.buf = StringIO()
+    self.old_stdout = sys.stdout
+    sys.stdout = self.buf
+
+  def tearDown(self):
+    sys.stdout = self.old_stdout
+
+  def testSimple(self):
+    grit.grit_runner.Main(['-i',
+                           util.PathFromRoot('grit/testdata/simple-input.xml'),
+                           'test', 'bla', 'voff', 'ga'])
+    output = self.buf.getvalue()
+    self.failUnless(output.count("'test'") == 0)  # tool name doesn't occur
+    self.failUnless(output.count('bla'))
+    self.failUnless(output.count('simple-input.xml'))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/lazy_re.py b/tools/grit/grit/lazy_re.py
new file mode 100644
index 0000000000..5c461e87e7
--- /dev/null
+++ b/tools/grit/grit/lazy_re.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''In GRIT, we used to compile a lot of regular expressions at parse
+time.  Since many of them never get used, we use lazy_re to compile
+them on demand the first time they are used, thus speeding up startup
+time in some cases.
+'''
+
+from __future__ import print_function
+
+import re
+
+
+class LazyRegexObject(object):
+  '''This object creates a RegexObject with the arguments passed in
+  its constructor, the first time any attribute except the several on
+  the class itself is accessed.  This accomplishes lazy compilation of
+  the regular expression while maintaining a nearly-identical
+  interface.
+  '''
+
+  def __init__(self, *args, **kwargs):
+    self._stash_args = args
+    self._stash_kwargs = kwargs
+    self._lazy_re = None
+
+  def _LazyInit(self):
+    if not self._lazy_re:
+      self._lazy_re = re.compile(*self._stash_args, **self._stash_kwargs)
+
+  def __getattribute__(self, name):
+    if name in ('_LazyInit', '_lazy_re', '_stash_args', '_stash_kwargs'):
+      return object.__getattribute__(self, name)
+    else:
+      self._LazyInit()
+      return getattr(self._lazy_re, name)
+
+
+def compile(*args, **kwargs):
+  '''Creates a LazyRegexObject that, when invoked on, will compile a
+  re.RegexObject (via re.compile) with the same arguments passed to
+  this function, and delegate almost all of its methods to it.
+  '''
+  return LazyRegexObject(*args, **kwargs)
diff --git a/tools/grit/grit/lazy_re_unittest.py b/tools/grit/grit/lazy_re_unittest.py
new file mode 100644
index 0000000000..8488b454ee
--- /dev/null
+++ b/tools/grit/grit/lazy_re_unittest.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit test for lazy_re.
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
+
+import re
+import unittest
+
+from grit import lazy_re
+
+
+class LazyReUnittest(unittest.TestCase):
+
+  def testCreatedOnlyOnDemand(self):
+    rex = lazy_re.compile('bingo')
+    self.assertEqual(None, rex._lazy_re)
+    self.assertTrue(rex.match('bingo'))
+    self.assertNotEqual(None, rex._lazy_re)
+
+  def testJustKwargsWork(self):
+    rex = lazy_re.compile(flags=re.I, pattern='BiNgO')
+    self.assertTrue(rex.match('bingo'))
+
+  def testPositionalAndKwargsWork(self):
+    rex = lazy_re.compile('BiNgO', flags=re.I)
+    self.assertTrue(rex.match('bingo'))
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/node/__init__.py b/tools/grit/grit/node/__init__.py
new file mode 100644
index 0000000000..2fc0d3360c
--- /dev/null
+++ b/tools/grit/grit/node/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Package 'grit.node'
+'''
+
+pass
diff --git a/tools/grit/grit/node/base.py b/tools/grit/grit/node/base.py
new file mode 100644
index 0000000000..40859d301d
--- /dev/null
+++ b/tools/grit/grit/node/base.py
@@ -0,0 +1,670 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Base types for nodes in a GRIT resource tree.
+'''
+
+from __future__ import print_function
+
+import ast
+import os
+import struct
+import sys
+from xml.sax import saxutils
+
+import six
+
+from grit import constants
+from grit import clique
+from grit import exception
+from grit import util
+from grit.node import brotli_util
+import grit.format.gzip_string
+
+
+class Node(object):
+  '''An item in the tree that has children.'''
+
+  # Valid content types that can be returned by _ContentType()
+  _CONTENT_TYPE_NONE = 0   # No CDATA content but may have children
+  _CONTENT_TYPE_CDATA = 1  # Only CDATA, no children.
+  _CONTENT_TYPE_MIXED = 2  # CDATA and children, possibly intermingled
+
+  # Types of files to be compressed by default.
+  _COMPRESS_BY_DEFAULT_EXTENSIONS = ('.js', '.html', '.css', '.svg')
+
+  # Default nodes to not whitelist skipped
+  _whitelist_marked_as_skip = False
+
+  # A class-static cache to speed up EvaluateExpression().
+  # Keys are expressions (e.g. 'is_ios and lang == "fr"'). Values are tuples
+  # (code, variables_in_expr) where code is the compiled expression and can be
+  # directly eval'd, and variables_in_expr is the list of variable and method
+  # names used in the expression (e.g. ['is_ios', 'lang']).
+  eval_expr_cache = {}
+
+  def __init__(self):
+    self.children = []        # A list of child elements
+    self.mixed_content = []   # A list of u'' and/or child elements (this
+    # duplicates 'children' but
+    # is needed to preserve markup-type content).
+    self.name = u''           # The name of this element
+    self.attrs = {}           # The set of attributes (keys to values)
+    self.parent = None        # Our parent unless we are the root element.
+    self.uberclique = None    # Allows overriding uberclique for parts of tree
+    self.source = None        # File that this node was parsed from
+
+  # This context handler allows you to write "with node:" and get a
+  # line identifying the offending node if an exception escapes from the body
+  # of the with statement.
+  def __enter__(self):
+    return self
+
+  def __exit__(self, exc_type, exc_value, traceback):
+    if exc_type is not None:
+      print(u'Error processing node %s: %s' % (six.text_type(self), exc_value))
+
+  def __iter__(self):
+    '''A preorder iteration through the tree that this node is the root of.'''
+    return self.Preorder()
+
+  def Preorder(self):
+    '''Generator that generates first this node, then the same generator for
+    any child nodes.'''
+    yield self
+    for child in self.children:
+      for iterchild in child.Preorder():
+        yield iterchild
+
+  def ActiveChildren(self):
+    '''Returns the children of this node that should be included in the current
+    configuration. Overridden by <if>.'''
+    return [node for node in self.children if not node.WhitelistMarkedAsSkip()]
+
+  def ActiveDescendants(self):
+    '''Yields the current node and all descendants that should be included in
+    the current configuration, in preorder.'''
+    yield self
+    for child in self.ActiveChildren():
+      for descendant in child.ActiveDescendants():
+        yield descendant
+
+  def GetRoot(self):
+    '''Returns the root Node in the tree this Node belongs to.'''
+    curr = self
+    while curr.parent:
+      curr = curr.parent
+    return curr
+
+    # TODO(joi) Use this (currently untested) optimization?:
+    #if hasattr(self, '_root'):
+    #  return self._root
+    #curr = self
+    #while curr.parent and not hasattr(curr, '_root'):
+    #  curr = curr.parent
+    #if curr.parent:
+    #  self._root = curr._root
+    #else:
+    #  self._root = curr
+    #return self._root
+
+  def StartParsing(self, name, parent):
+    '''Called at the start of parsing.
+
+    Args:
+      name: u'elementname'
+      parent: grit.node.base.Node or subclass or None
+    '''
+    assert isinstance(name, six.string_types)
+    assert not parent or isinstance(parent, Node)
+    self.name = name
+    self.parent = parent
+
+  def AddChild(self, child):
+    '''Adds a child to the list of children of this node, if it is a valid
+    child for the node.'''
+    assert isinstance(child, Node)
+    if (not self._IsValidChild(child) or
+        self._ContentType() == self._CONTENT_TYPE_CDATA):
+      explanation = 'invalid child %s for parent %s' % (str(child), self.name)
+      raise exception.UnexpectedChild(explanation)
+    self.children.append(child)
+    self.mixed_content.append(child)
+
+  def RemoveChild(self, child_id):
+    '''Removes the first node that has a "name" attribute which
+    matches "child_id" in the list of immediate children of
+    this node.
+
+    Args:
+      child_id: String identifying the child to be removed
+    '''
+    index = 0
+    # Safe not to copy since we only remove the first element found
+    for child in self.children:
+      name_attr = child.attrs['name']
+      if name_attr == child_id:
+        self.children.pop(index)
+        self.mixed_content.pop(index)
+        break
+      index += 1
+
+  def AppendContent(self, content):
+    '''Appends a chunk of text as content of this node.
+
+    Args:
+      content: u'hello'
+
+    Return:
+      None
+    '''
+    assert isinstance(content, six.string_types)
+    if self._ContentType() != self._CONTENT_TYPE_NONE:
+      self.mixed_content.append(content)
+    elif content.strip() != '':
+      raise exception.UnexpectedContent()
+
+  def HandleAttribute(self, attrib, value):
+    '''Informs the node of an attribute that was parsed out of the GRD file
+    for it.
+
+    Args:
+      attrib: 'name'
+      value: 'fooblat'
+
+    Return:
+      None
+    '''
+    assert isinstance(attrib, six.string_types)
+    assert isinstance(value, six.string_types)
+    if self._IsValidAttribute(attrib, value):
+      self.attrs[attrib] = value
+    else:
+      raise exception.UnexpectedAttribute(attrib)
+
+  def EndParsing(self):
+    '''Called at the end of parsing.'''
+
+    # TODO(joi) Rewrite this, it's extremely ugly!
+    if len(self.mixed_content):
+      if isinstance(self.mixed_content[0], six.string_types):
+        # Remove leading and trailing chunks of pure whitespace.
+        while (len(self.mixed_content) and
+               isinstance(self.mixed_content[0], six.string_types) and
+               self.mixed_content[0].strip() == ''):
+          self.mixed_content = self.mixed_content[1:]
+        # Strip leading and trailing whitespace from mixed content chunks
+        # at front and back.
+        if (len(self.mixed_content) and
+            isinstance(self.mixed_content[0], six.string_types)):
+          self.mixed_content[0] = self.mixed_content[0].lstrip()
+        # Remove leading and trailing ''' (used to demarcate whitespace)
+        if (len(self.mixed_content) and
+            isinstance(self.mixed_content[0], six.string_types)):
+          if self.mixed_content[0].startswith("'''"):
+            self.mixed_content[0] = self.mixed_content[0][3:]
+    if len(self.mixed_content):
+      if isinstance(self.mixed_content[-1], six.string_types):
+        # Same stuff all over again for the tail end.
+        while (len(self.mixed_content) and
+               isinstance(self.mixed_content[-1], six.string_types) and
+               self.mixed_content[-1].strip() == ''):
+          self.mixed_content = self.mixed_content[:-1]
+        if (len(self.mixed_content) and
+            isinstance(self.mixed_content[-1], six.string_types)):
+          self.mixed_content[-1] = self.mixed_content[-1].rstrip()
+        if (len(self.mixed_content) and
+            isinstance(self.mixed_content[-1], six.string_types)):
+          if self.mixed_content[-1].endswith("'''"):
+            self.mixed_content[-1] = self.mixed_content[-1][:-3]
+
+    # Check that all mandatory attributes are there.
+    for node_mandatt in self.MandatoryAttributes():
+      mandatt_list = []
+      if node_mandatt.find('|') >= 0:
+        mandatt_list = node_mandatt.split('|')
+      else:
+        mandatt_list.append(node_mandatt)
+
+      mandatt_option_found = False
+      for mandatt in mandatt_list:
+        assert mandatt not in self.DefaultAttributes()
+        if mandatt in self.attrs:
+          if not mandatt_option_found:
+            mandatt_option_found = True
+          else:
+            raise exception.MutuallyExclusiveMandatoryAttribute(mandatt)
+
+      if not mandatt_option_found:
+        raise exception.MissingMandatoryAttribute(mandatt)
+
+    # Add default attributes if not specified in input file.
+    for defattr in self.DefaultAttributes():
+      if not defattr in self.attrs:
+        self.attrs[defattr] = self.DefaultAttributes()[defattr]
+
+  def GetCdata(self):
+    '''Returns all CDATA of this element, concatenated into a single
+    string.  Note that this ignores any elements embedded in CDATA.'''
+    return ''.join([c for c in self.mixed_content
+                    if isinstance(c, six.string_types)])
+
+  def __str__(self):
+    '''Returns this node and all nodes below it as an XML document in a Unicode
+    string.'''
+    header = u'<?xml version="1.0" encoding="UTF-8"?>\n'
+    return header + self.FormatXml()
+
+  # Some Python 2 glue.
+  __unicode__ = __str__
+
+  def FormatXml(self, indent = u'', one_line = False):
+    '''Returns this node and all nodes below it as an XML
+    element in a Unicode string.  This differs from __unicode__ in that it does
+    not include the <?xml> stuff at the top of the string.  If one_line is true,
+    children and CDATA are layed out in a way that preserves internal
+    whitespace.
+    '''
+    assert isinstance(indent, six.string_types)
+
+    content_one_line = (one_line or
+                        self._ContentType() == self._CONTENT_TYPE_MIXED)
+    inside_content = self.ContentsAsXml(indent, content_one_line)
+
+    # Then the attributes for this node.
+    attribs = u''
+    default_attribs = self.DefaultAttributes()
+    for attrib, value in sorted(self.attrs.items()):
+      # Only print an attribute if it is other than the default value.
+      if attrib not in default_attribs or value != default_attribs[attrib]:
+        attribs += u' %s=%s' % (attrib, saxutils.quoteattr(value))
+
+    # Finally build the XML for our node and return it
+    if len(inside_content) > 0:
+      if one_line:
+        return u'<%s%s>%s</%s>' % (self.name, attribs, inside_content,
+                                   self.name)
+      elif content_one_line:
+        return u'%s<%s%s>\n%s  %s\n%s</%s>' % (
+          indent, self.name, attribs,
+          indent, inside_content,
+          indent, self.name)
+      else:
+        return u'%s<%s%s>\n%s\n%s</%s>' % (
+          indent, self.name, attribs,
+          inside_content,
+          indent, self.name)
+    else:
+      return u'%s<%s%s />' % (indent, self.name, attribs)
+
+  def ContentsAsXml(self, indent, one_line):
+    '''Returns the contents of this node (CDATA and child elements) in XML
+    format.  If 'one_line' is true, the content will be laid out on one line.'''
+    assert isinstance(indent, six.string_types)
+
+    # Build the contents of the element.
+    inside_parts = []
+    last_item = None
+    for mixed_item in self.mixed_content:
+      if isinstance(mixed_item, Node):
+        inside_parts.append(mixed_item.FormatXml(indent + u'  ', one_line))
+        if not one_line:
+          inside_parts.append(u'\n')
+      else:
+        message = mixed_item
+        # If this is the first item and it starts with whitespace, we add
+        # the ''' delimiter.
+        if not last_item and message.lstrip() != message:
+          message = u"'''" + message
+        inside_parts.append(util.EncodeCdata(message))
+      last_item = mixed_item
+
+    # If there are only child nodes and no cdata, there will be a spurious
+    # trailing \n
+    if len(inside_parts) and inside_parts[-1] == '\n':
+      inside_parts = inside_parts[:-1]
+
+    # If the last item is a string (not a node) and ends with whitespace,
+    # we need to add the ''' delimiter.
+    if (isinstance(last_item, six.string_types) and
+        last_item.rstrip() != last_item):
+      inside_parts[-1] = inside_parts[-1] + u"'''"
+
+    return u''.join(inside_parts)
+
+  def SubstituteMessages(self, substituter):
+    '''Applies substitutions to all messages in the tree.
+
+    Called as a final step of RunGatherers.
+
+    Args:
+      substituter: a grit.util.Substituter object.
+    '''
+    for child in self.children:
+      child.SubstituteMessages(substituter)
+
+  def _IsValidChild(self, child):
+    '''Returns true if 'child' is a valid child of this node.
+    Overridden by subclasses.'''
+    return False
+
+  def _IsValidAttribute(self, name, value):
+    '''Returns true if 'name' is the name of a valid attribute of this element
+    and 'value' is a valid value for that attribute.  Overriden by
+    subclasses unless they have only mandatory attributes.'''
+    return (name in self.MandatoryAttributes() or
+            name in self.DefaultAttributes())
+
+  def _ContentType(self):
+    '''Returns the type of content this element can have.  Overridden by
+    subclasses.  The content type can be one of the _CONTENT_TYPE_XXX constants
+    above.'''
+    return self._CONTENT_TYPE_NONE
+
+  def MandatoryAttributes(self):
+    '''Returns a list of attribute names that are mandatory (non-optional)
+    on the current element. One can specify a list of
+    "mutually exclusive mandatory" attributes by specifying them as one
+    element in the list, separated by a "|" character.
+    '''
+    return []
+
+  def DefaultAttributes(self):
+    '''Returns a dictionary of attribute names that have defaults, mapped to
+    the default value.  Overridden by subclasses.'''
+    return {}
+
+  def GetCliques(self):
+    '''Returns all MessageClique objects belonging to this node.  Overridden
+    by subclasses.
+
+    Return:
+      [clique1, clique2] or []
+    '''
+    return []
+
+  def ToRealPath(self, path_from_basedir):
+    '''Returns a real path (which can be absolute or relative to the current
+    working directory), given a path that is relative to the base directory
+    set for the GRIT input file.
+
+    Args:
+      path_from_basedir: '..'
+
+    Return:
+      'resource'
+    '''
+    return util.normpath(os.path.join(self.GetRoot().GetBaseDir(),
+                                      os.path.expandvars(path_from_basedir)))
+
+  def GetInputPath(self):
+    '''Returns a path, relative to the base directory set for the grd file,
+    that points to the file the node refers to.
+    '''
+    # This implementation works for most nodes that have an input file.
+    return self.attrs['file']
+
+  def UberClique(self):
+    '''Returns the uberclique that should be used for messages originating in
+    a given node.  If the node itself has its uberclique set, that is what we
+    use, otherwise we search upwards until we find one.  If we do not find one
+    even at the root node, we set the root node's uberclique to a new
+    uberclique instance.
+    '''
+    node = self
+    while not node.uberclique and node.parent:
+      node = node.parent
+    if not node.uberclique:
+      node.uberclique = clique.UberClique()
+    return node.uberclique
+
+  def IsTranslateable(self):
+    '''Returns false if the node has contents that should not be translated,
+    otherwise returns false (even if the node has no contents).
+    '''
+    if not 'translateable' in self.attrs:
+      return True
+    else:
+      return self.attrs['translateable'] == 'true'
+
+  def IsAccessibilityWithNoUI(self):
+    '''Returns true if the node is marked as an accessibility label and the
+    message isn't shown in the UI. Otherwise returns false. This label is
+    used to determine if the text requires screenshots.'''
+    if not 'is_accessibility_with_no_ui' in self.attrs:
+      return False
+    else:
+      return self.attrs['is_accessibility_with_no_ui'] == 'true'
+
+  def GetNodeById(self, id):
+    '''Returns the node in the subtree parented by this node that has a 'name'
+    attribute matching 'id'.  Returns None if no such node is found.
+    '''
+    for node in self:
+      if 'name' in node.attrs and node.attrs['name'] == id:
+        return node
+    return None
+
+  def GetChildrenOfType(self, type):
+    '''Returns a list of all subnodes (recursing to all leaves) of this node
+    that are of the indicated type (or tuple of types).
+
+    Args:
+      type: A type you could use with isinstance().
+
+    Return:
+      A list, possibly empty.
+    '''
+    return [child for child in self if isinstance(child, type)]
+
+  def GetTextualIds(self):
+    '''Returns a list of the textual ids of this node.
+    '''
+    if 'name' in self.attrs:
+      return [self.attrs['name']]
+    return []
+
+  @classmethod
+  def EvaluateExpression(cls, expr, defs, target_platform, extra_variables={}):
+    '''Worker for EvaluateCondition (below) and conditions in XTB files.'''
+    if expr in cls.eval_expr_cache:
+      code, variables_in_expr = cls.eval_expr_cache[expr]
+    else:
+      # Get a list of all variable and method names used in the expression.
+      syntax_tree = ast.parse(expr, mode='eval')
+      variables_in_expr = [node.id for node in ast.walk(syntax_tree) if
+          isinstance(node, ast.Name) and node.id not in ('True', 'False')]
+      code = compile(syntax_tree, filename='<string>', mode='eval')
+      cls.eval_expr_cache[expr] = code, variables_in_expr
+
+    # Set values only for variables that are needed to eval the expression.
+    variable_map = {}
+    for name in variables_in_expr:
+      if name == 'os':
+        value = target_platform
+      elif name == 'defs':
+        value = defs
+
+      elif name == 'is_linux':
+        value = target_platform.startswith('linux')
+      elif name == 'is_macosx':
+        value = target_platform == 'darwin'
+      elif name == 'is_win':
+        value = target_platform in ('cygwin', 'win32')
+      elif name == 'is_android':
+        value = target_platform == 'android'
+      elif name == 'is_ios':
+        value = target_platform == 'ios'
+      elif name == 'is_bsd':
+        value = 'bsd' in target_platform
+      elif name == 'is_posix':
+        value = (target_platform in ('darwin', 'linux2', 'linux3', 'sunos5',
+                                     'android', 'ios')
+                 or 'bsd' in target_platform)
+
+      elif name == 'pp_ifdef':
+        def pp_ifdef(symbol):
+          return symbol in defs
+        value = pp_ifdef
+      elif name == 'pp_if':
+        def pp_if(symbol):
+          return defs.get(symbol, False)
+        value = pp_if
+
+      elif name in defs:
+        value = defs[name]
+      elif name in extra_variables:
+        value = extra_variables[name]
+      else:
+        # Undefined variables default to False.
+        value = False
+
+      variable_map[name] = value
+
+    eval_result = eval(code, {}, variable_map)
+    assert isinstance(eval_result, bool)
+    return eval_result
+
+  def EvaluateCondition(self, expr):
+    '''Returns true if and only if the Python expression 'expr' evaluates
+    to true.
+
+    The expression is given a few local variables:
+      - 'lang' is the language currently being output
+           (the 'lang' attribute of the <output> element).
+      - 'context' is the current output context
+           (the 'context' attribute of the <output> element).
+      - 'defs' is a map of C preprocessor-style symbol names to their values.
+      - 'os' is the current platform (likely 'linux2', 'win32' or 'darwin').
+      - 'pp_ifdef(symbol)' is a shorthand for "symbol in defs".
+      - 'pp_if(symbol)' is a shorthand for "symbol in defs and defs[symbol]".
+      - 'is_linux', 'is_macosx', 'is_win', 'is_posix' are true if 'os'
+           matches the given platform.
+    '''
+    root = self.GetRoot()
+    lang = getattr(root, 'output_language', '')
+    context = getattr(root, 'output_context', '')
+    defs = getattr(root, 'defines', {})
+    target_platform = getattr(root, 'target_platform', '')
+    extra_variables = {
+        'lang': lang,
+        'context': context,
+    }
+    return Node.EvaluateExpression(
+        expr, defs, target_platform, extra_variables)
+
+  def OnlyTheseTranslations(self, languages):
+    '''Turns off loading of translations for languages not in the provided list.
+
+    Attrs:
+      languages: ['fr', 'zh_cn']
+    '''
+    for node in self:
+      if (hasattr(node, 'IsTranslation') and
+          node.IsTranslation() and
+          node.GetLang() not in languages):
+        node.DisableLoading()
+
+  def FindBooleanAttribute(self, attr, default, skip_self):
+    '''Searches all ancestors of the current node for the nearest enclosing
+    definition of the given boolean attribute.
+
+    Args:
+      attr: 'fallback_to_english'
+      default: What to return if no node defines the attribute.
+      skip_self: Don't check the current node, only its parents.
+    '''
+    p = self.parent if skip_self else self
+    while p:
+      value = p.attrs.get(attr, 'default').lower()
+      if value != 'default':
+        return (value == 'true')
+      p = p.parent
+    return default
+
+  def PseudoIsAllowed(self):
+    '''Returns true if this node is allowed to use pseudo-translations.  This
+    is true by default, unless this node is within a <release> node that has
+    the allow_pseudo attribute set to false.
+    '''
+    return self.FindBooleanAttribute('allow_pseudo',
+                                     default=True, skip_self=True)
+
+  def ShouldFallbackToEnglish(self):
+    '''Returns true iff this node should fall back to English when
+    pseudotranslations are disabled and no translation is available for a
+    given message.
+    '''
+    return self.FindBooleanAttribute('fallback_to_english',
+                                     default=False, skip_self=True)
+
+  def WhitelistMarkedAsSkip(self):
+    '''Returns true if the node is marked to be skipped in the output by a
+    whitelist.
+    '''
+    return self._whitelist_marked_as_skip
+
+  def SetWhitelistMarkedAsSkip(self, mark_skipped):
+    '''Sets WhitelistMarkedAsSkip.
+    '''
+    self._whitelist_marked_as_skip = mark_skipped
+
+  def ExpandVariables(self):
+    '''Whether we need to expand variables on a given node.'''
+    return False
+
+  def IsResourceMapSource(self):
+    '''Whether this node is a resource map source.'''
+    return False
+
+  def CompressDataIfNeeded(self, data):
+    '''Compress data using the format specified in the compress attribute.
+
+    Args:
+      data: The data to compressed.
+    Returns:
+      The data in gzipped or brotli compressed format. If the format is
+      unspecified then this returns the data uncompressed.
+    '''
+
+    compress = self.attrs.get('compress')
+
+    # Compress JS, HTML, CSS and SVG files by default (gzip), unless |compress|
+    # is explicitly specified.
+    compress_by_default = (compress == 'default'
+                           and self.attrs.get('file').endswith(
+                               self._COMPRESS_BY_DEFAULT_EXTENSIONS))
+
+    if compress == 'gzip' or compress_by_default:
+      # We only use rsyncable compression on Linux.
+      # We exclude ChromeOS since ChromeOS bots are Linux based but do not have
+      # the --rsyncable option built in for gzip. See crbug.com/617950.
+      if sys.platform == 'linux2' and 'chromeos' not in self.GetRoot().defines:
+        return grit.format.gzip_string.GzipStringRsyncable(data)
+      return grit.format.gzip_string.GzipString(data)
+
+    if compress == 'brotli':
+      # The length of the uncompressed data as 8 bytes little-endian.
+      size_bytes = struct.pack("<q", len(data))
+      data = brotli_util.BrotliCompress(data)
+      # BROTLI_CONST is prepended to brotli decompressed data in order to
+      # easily check if a resource has been brotli compressed.
+      # The length of the uncompressed data is also appended to the start,
+      # truncated to 6 bytes, little-endian. size_bytes is 8 bytes,
+      # need to truncate further to 6.
+      formatter = b'%ds %dx %ds' % (6, 2, len(size_bytes) - 8)
+      return (constants.BROTLI_CONST +
+             b''.join(struct.unpack(formatter, size_bytes)) +
+             data)
+
+    if compress == 'false' or compress == 'default':
+      return data
+
+    raise Exception('Invalid value for compression')
+
+
+class ContentNode(Node):
+  '''Convenience baseclass for nodes that can have content.'''
+  def _ContentType(self):
+    return self._CONTENT_TYPE_MIXED
diff --git a/tools/grit/grit/node/base_unittest.py b/tools/grit/grit/node/base_unittest.py
new file mode 100644
index 0000000000..32a5a0ca59
--- /dev/null
+++ b/tools/grit/grit/node/base_unittest.py
@@ -0,0 +1,259 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Unit tests for base.Node functionality (as used in various subclasses)'''
+
+from __future__ import print_function
+
+import os
+import sys
+if __name__ == '__main__':
+  sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+import unittest
+
+from six import StringIO
+
+from grit import grd_reader
+from grit import util
+from grit.node import base
+from grit.node import message
+
+
+def MakePlaceholder(phname='BINGO'):
+  ph = message.PhNode()
+  ph.StartParsing(u'ph', None)
+  ph.HandleAttribute(u'name', phname)
+  ph.AppendContent(u'bongo')
+  ph.EndParsing()
+  return ph
+
+
+class NodeUnittest(unittest.TestCase):
+  def testWhitespaceHandling(self):
+    # We test using the Message node type.
+    node = message.MessageNode()
+    node.StartParsing(u'hello', None)
+    node.HandleAttribute(u'name', u'bla')
+    node.AppendContent(u" '''  two spaces  ")
+    node.EndParsing()
+    self.failUnless(node.GetCdata() == u'  two spaces')
+
+    node = message.MessageNode()
+    node.StartParsing(u'message', None)
+    node.HandleAttribute(u'name', u'bla')
+    node.AppendContent(u"  two spaces  '''  ")
+    node.EndParsing()
+    self.failUnless(node.GetCdata() == u'two spaces  ')
+
+  def testWhitespaceHandlingWithChildren(self):
+    # We test using the Message node type.
+    node = message.MessageNode()
+    node.StartParsing(u'message', None)
+    node.HandleAttribute(u'name', u'bla')
+    node.AppendContent(u" '''  two spaces  ")
+    node.AddChild(MakePlaceholder())
+    node.AppendContent(u' space before and after ')
+    node.AddChild(MakePlaceholder('BONGO'))
+    node.AppendContent(u" space before two after  '''")
+    node.EndParsing()
+    self.failUnless(node.mixed_content[0] == u'  two spaces  ')
+    self.failUnless(node.mixed_content[2] == u' space before and after ')
+    self.failUnless(node.mixed_content[-1] == u' space before two after  ')
+
+  def testXmlFormatMixedContent(self):
+    # Again test using the Message node type, because it is the only mixed
+    # content node.
+    node = message.MessageNode()
+    node.StartParsing(u'message', None)
+    node.HandleAttribute(u'name', u'name')
+    node.AppendContent(u'Hello <young> ')
+
+    ph = message.PhNode()
+    ph.StartParsing(u'ph', None)
+    ph.HandleAttribute(u'name', u'USERNAME')
+    ph.AppendContent(u'$1')
+    ex = message.ExNode()
+    ex.StartParsing(u'ex', None)
+    ex.AppendContent(u'Joi')
+    ex.EndParsing()
+    ph.AddChild(ex)
+    ph.EndParsing()
+
+    node.AddChild(ph)
+    node.EndParsing()
+
+    non_indented_xml = node.FormatXml()
+    self.failUnless(non_indented_xml == u'<message name="name">\n  Hello '
+                    u'<young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+                    u'\n</message>')
+
+    indented_xml = node.FormatXml(u'  ')
+    self.failUnless(indented_xml == u'  <message name="name">\n    Hello '
+                    u'<young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+                    u'\n  </message>')
+
+  def testXmlFormatMixedContentWithLeadingWhitespace(self):
+    # Again test using the Message node type, because it is the only mixed
+    # content node.
+    node = message.MessageNode()
+    node.StartParsing(u'message', None)
+    node.HandleAttribute(u'name', u'name')
+    node.AppendContent(u"'''   Hello <young> ")
+
+    ph = message.PhNode()
+    ph.StartParsing(u'ph', None)
+    ph.HandleAttribute(u'name', u'USERNAME')
+    ph.AppendContent(u'$1')
+    ex = message.ExNode()
+    ex.StartParsing(u'ex', None)
+    ex.AppendContent(u'Joi')
+    ex.EndParsing()
+    ph.AddChild(ex)
+    ph.EndParsing()
+
+    node.AddChild(ph)
+    node.AppendContent(u" yessiree '''")
+    node.EndParsing()
+
+    non_indented_xml = node.FormatXml()
+    self.failUnless(non_indented_xml ==
+                    u"<message name=\"name\">\n  '''   Hello"
+                    u' <young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+                    u" yessiree '''\n</message>")
+
+    indented_xml = node.FormatXml(u'  ')
+    self.failUnless(indented_xml ==
+                    u"  <message name=\"name\">\n    '''   Hello"
+                    u' <young> <ph name="USERNAME">$1<ex>Joi</ex></ph>'
+                    u" yessiree '''\n  </message>")
+
+    self.failUnless(node.GetNodeById('name'))
+
+  def testXmlFormatContentWithEntities(self):
+    '''Tests a bug where   would not be escaped correctly.'''
+    from grit import tclib
+    msg_node = message.MessageNode.Construct(None, tclib.Message(
+      text = 'BEGIN_BOLDHelloWHITESPACEthere!END_BOLD Bingo!',
+      placeholders = [
+        tclib.Placeholder('BEGIN_BOLD', '<b>', 'bla'),
+        tclib.Placeholder('WHITESPACE', ' ', 'bla'),
+        tclib.Placeholder('END_BOLD', '</b>', 'bla')]),
+                                             'BINGOBONGO')
+    xml = msg_node.FormatXml()
+    self.failUnless(xml.find(' ') == -1, 'should have no entities')
+
+  def testIter(self):
+    # First build a little tree of message and ph nodes.
+    node = message.MessageNode()
+    node.StartParsing(u'message', None)
+    node.HandleAttribute(u'name', u'bla')
+    node.AppendContent(u" '''  two spaces  ")
+    node.AppendContent(u' space before and after ')
+    ph = message.PhNode()
+    ph.StartParsing(u'ph', None)
+    ph.AddChild(message.ExNode())
+    ph.HandleAttribute(u'name', u'BINGO')
+    ph.AppendContent(u'bongo')
+    node.AddChild(ph)
+    node.AddChild(message.PhNode())
+    node.AppendContent(u" space before two after  '''")
+
+    order = [message.MessageNode, message.PhNode, message.ExNode, message.PhNode]
+    for n in node:
+      self.failUnless(type(n) == order[0])
+      order = order[1:]
+    self.failUnless(len(order) == 0)
+
+  def testGetChildrenOfType(self):
+    xml = '''<?xml version="1.0" encoding="UTF-8"?>
+      <grit latest_public_release="2" source_lang_id="en-US"
+            current_release="3" base_dir=".">
+        <outputs>
+          <output filename="resource.h" type="rc_header" />
+          <output filename="en/generated_resources.rc" type="rc_all"
+                  lang="en" />
+          <if expr="pp_if('NOT_TRUE')">
+            <output filename="de/generated_resources.rc" type="rc_all"
+                    lang="de" />
+          </if>
+        </outputs>
+        <release seq="3">
+          <messages>
+            <message name="ID_HELLO">Hello!</message>
+          </messages>
+        </release>
+      </grit>'''
+    grd = grd_reader.Parse(StringIO(xml),
+                           util.PathFromRoot('grit/test/data'))
+    from grit.node import node_io
+    output_nodes = grd.GetChildrenOfType(node_io.OutputNode)
+    self.failUnlessEqual(len(output_nodes), 3)
+    self.failUnlessEqual(output_nodes[2].attrs['filename'],
+                         'de/generated_resources.rc')
+
+  def testEvaluateExpression(self):
+    def AssertExpr(expected_value, expr, defs, target_platform,
+                   extra_variables):
+      self.failUnlessEqual(expected_value, base.Node.EvaluateExpression(
+          expr, defs, target_platform, extra_variables))
+
+    AssertExpr(True, "True", {}, 'linux', {})
+    AssertExpr(False, "False", {}, 'linux', {})
+    AssertExpr(True, "True or False", {}, 'linux', {})
+    AssertExpr(False, "True and False", {}, 'linux', {})
+    AssertExpr(True, "os == 'linux'", {}, 'linux', {})
+    AssertExpr(False, "os == 'linux'", {}, 'ios', {})
+    AssertExpr(True, "'foo' in defs", {'foo': 'bar'}, 'ios', {})
+    AssertExpr(False, "'foo' in defs", {'baz': 'bar'}, 'ios', {})
+    AssertExpr(False, "'foo' in defs", {}, 'ios', {})
+    AssertExpr(True, "is_linux", {}, 'linux2', {})
+    AssertExpr(False, "is_linux", {}, 'win32', {})
+    AssertExpr(True, "is_macosx", {}, 'darwin', {})
+    AssertExpr(False, "is_macosx", {}, 'ios', {})
+    AssertExpr(True, "is_win", {}, 'win32', {})
+    AssertExpr(False, "is_win", {}, 'darwin', {})
+    AssertExpr(True, "is_android", {}, 'android', {})
+    AssertExpr(False, "is_android", {}, 'linux3', {})
+    AssertExpr(True, "is_ios", {}, 'ios', {})
+    AssertExpr(False, "is_ios", {}, 'darwin', {})
+    AssertExpr(True, "is_posix", {}, 'linux2', {})
+    AssertExpr(True, "is_posix", {}, 'darwin', {})
+    AssertExpr(True, "is_posix", {}, 'android', {})
+    AssertExpr(True, "is_posix", {}, 'ios', {})
+    AssertExpr(True, "is_posix", {}, 'freebsd7', {})
+    AssertExpr(False, "is_posix", {}, 'win32', {})
+    AssertExpr(True, "pp_ifdef('foo')", {'foo': True}, 'win32', {})
+    AssertExpr(True, "pp_ifdef('foo')", {'foo': False}, 'win32', {})
+    AssertExpr(False, "pp_ifdef('foo')", {'bar': True}, 'win32', {})
+    AssertExpr(True, "pp_if('foo')", {'foo': True}, 'win32', {})
+    AssertExpr(False, "pp_if('foo')", {'foo': False}, 'win32', {})
+    AssertExpr(False, "pp_if('foo')", {'bar': True}, 'win32', {})
+    AssertExpr(True, "foo", {'foo': True}, 'win32', {})
+    AssertExpr(False, "foo", {'foo': False}, 'win32', {})
+    AssertExpr(False, "foo", {'bar': True}, 'win32', {})
+    AssertExpr(True, "foo == 'baz'", {'foo': 'baz'}, 'win32', {})
+    AssertExpr(False, "foo == 'baz'", {'foo': True}, 'win32', {})
+    AssertExpr(False, "foo == 'baz'", {}, 'win32', {})
+    AssertExpr(True, "lang == 'de'", {}, 'win32', {'lang': 'de'})
+    AssertExpr(False, "lang == 'de'", {}, 'win32', {'lang': 'fr'})
+    AssertExpr(False, "lang == 'de'", {}, 'win32', {})
+
+    # Test a couple more complex expressions for good measure.
+    AssertExpr(True, "is_ios and (lang in ['de', 'fr'] or foo)",
+               {'foo': 'bar'}, 'ios', {'lang': 'fr', 'context': 'today'})
+    AssertExpr(False, "is_ios and (lang in ['de', 'fr'] or foo)",
+               {'foo': False}, 'linux2', {'lang': 'fr', 'context': 'today'})
+    AssertExpr(False, "is_ios and (lang in ['de', 'fr'] or foo)",
+               {'baz': 'bar'}, 'ios', {'lang': 'he', 'context': 'today'})
+    AssertExpr(True, "foo == 'bar' or not baz",
+               {'foo': 'bar', 'fun': True}, 'ios', {'lang': 'en'})
+    AssertExpr(True, "foo == 'bar' or not baz",
+               {}, 'ios', {'lang': 'en', 'context': 'java'})
+    AssertExpr(False, "foo == 'bar' or not baz",
+               {'foo': 'ruz', 'baz': True}, 'ios', {'lang': 'en'})
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/grit/grit/node/brotli_util.py b/tools/grit/grit/node/brotli_util.py
new file mode 100644
index 0000000000..77f70e49d5
--- /dev/null
+++ b/tools/grit/grit/node/brotli_util.py
@@ -0,0 +1,29 @@
+# Copyright 2019 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Framework for compressing resources using Brotli."""
+
+import subprocess
+
+__brotli_executable = None
+
+
+def SetBrotliCommand(brotli):
+  # brotli is a list. In production it contains the path to the Brotli executable.
+  # During testing it contains [python, mock_brotli.py] for testing on Windows.
+  global __brotli_executable
+  __brotli_executable = brotli
+
+
+def BrotliCompress(data):
+  if not __brotli_executable:
+    raise Exception('Add "use_brotli = true" to you GN grit(...) target ' +
+                    'if you want to use brotli.')
+  compress = subprocess.Popen(__brotli_executable + ['-', '-f'],
+                              stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+  return compress.communicate(data)[0]
+
+def IsInitialized():
+  global __brotli_executable
+  return __brotli_executable is not None
diff --git a/tools/grit/grit/node/custom/__init__.py b/tools/grit/grit/node/custom/__init__.py
new file mode 100644
index 0000000000..e179cf7730
--- /dev/null
+++ b/tools/grit/grit/node/custom/__init__.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''Package 'grit.node.custom'
+'''
+
+pass
diff --git a/tools/grit/grit/node/custom/filename.py b/tools/grit/grit/node/custom/filename.py
new file mode 100644
index 0000000000..55a27e58c1
--- /dev/null
+++ b/tools/grit/grit/node/custom/filename.py
@@ -0,0 +1,29 @@
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+'''A CustomType for filenames.'''
+
+from __future__ import print_function
+
+from grit import clique
+from grit import lazy_re
+
+
+class WindowsFilename(clique.CustomType):
+  '''Validates that messages can be used as Windows filenames, and strips
+  illegal characters out of translations.
+  '''
+
+  BANNED = lazy_re.compile(r'\+|:|\/|\\\\|\*|\?|\"|\<|\>|\|')
+
+  def Validate(self, message):
+    return not self.BANNED.search(message.GetPresentableContent())
+
+  def ValidateAndModify(self, lang, translation):
+    is_ok = self.Validate(translation)
+    self.ModifyEachTextPart(lang, translation)
+    return is_ok
+
+  def ModifyTextPart(self, lang, text):
+    return self.BANNED.sub(' ', text)
diff --git a/tools/grit/grit/node/custom/filename_unittest.py b/tools/grit/grit/node/custom/filename_unittest.py
new file mode 100644
index 0000000000..8e2a6dd64a
--- /dev/null
+++ b/tools/grit/grit/node/custom/filename_unittest.py
@@ -0,0 +1,34 @@
+#!/usr/bin/env python
+# Copyright (c) 2012 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
--> --------------------

--> maximum size reached

--> --------------------

[Verzeichnis aufwärts0.932unsichere VerbindungÜbersetzung europäischer Sprachen durch Browser2026-04-26]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge