Repository: oppia/oppia

Issue:
Increasing unit test coverage for build.py
At the point of writing this, coverage for build.py sits at 25%. And more codes are getting added from PR #5468 . As stated toward the end of that PR, having a good coverage for build.py is key in term of ensuring changes to its function does not break basic building function.
 



Discussion:

Resolving patch:
diff --git a/scripts/build.py b/scripts/build.py
--- a/scripts/build.py
+++ b/scripts/build.py
@@ -16,7 +16,6 @@
 
 import collections
 import fnmatch
-import glob
 import hashlib
 import json
 import optparse
@@ -31,23 +30,33 @@
 
 THIRD_PARTY_STATIC_DIR = os.path.join('third_party', 'static')
 THIRD_PARTY_GENERATED_DEV_DIR = os.path.join('third_party', 'generated', '')
-THIRD_PARTY_GENERATED_STAGING_DIR = os.path.join(
-    'backend_prod_files', 'third_party', 'generated', '')
 THIRD_PARTY_GENERATED_OUT_DIR = os.path.join(
     'build', 'third_party', 'generated', '')
 
-EXTENSIONS_DEV_DIR = os.path.join('extensions', '')
-EXTENSIONS_STAGING_DIR = (
-    os.path.join('backend_prod_files', 'extensions', ''))
-EXTENSIONS_OUT_DIR = os.path.join('build', 'extensions', '')
+THIRD_PARTY_JS_RELATIVE_FILEPATH = os.path.join('js', 'third_party.js')
+MINIFIED_THIRD_PARTY_JS_RELATIVE_FILEPATH = os.path.join(
+    'js', 'third_party.min.js')
 
-TEMPLATES_DEV_DIR = os.path.join('templates', 'dev', 'head', '')
-TEMPLATES_DEV_DIR_CORE = os.path.join('core', 'templates', 'dev', 'head', '')
-TEMPLATES_STAGING_DIR = (
-    os.path.join('backend_prod_files', 'templates', 'head', ''))
-TEMPLATES_OUT_DIR = os.path.join('build', 'templates', 'head', '')
+THIRD_PARTY_CSS_RELATIVE_FILEPATH = os.path.join('css', 'third_party.css')
+MINIFIED_THIRD_PARTY_CSS_RELATIVE_FILEPATH = os.path.join(
+    'css', 'third_party.min.css')
+
+FONTS_RELATIVE_DIRECTORY_PATH = os.path.join('fonts', '')
 
-HASHES_JSON = os.path.join('build', 'assets', 'hashes.js')
+EXTENSIONS_DIRNAMES_TO_DIRPATHS = {
+    'dev_dir': os.path.join('extensions', ''),
+    'staging_dir': os.path.join('backend_prod_files', 'extensions', ''),
+    'out_dir': os.path.join('build', 'extensions', '')
+}
+TEMPLATES_DEV_DIR = os.path.join('templates', 'dev', 'head', '')
+TEMPLATES_CORE_DIRNAMES_TO_DIRPATHS = {
+    'dev_dir': os.path.join('core', 'templates', 'dev', 'head', ''),
+    'staging_dir': os.path.join('backend_prod_files', 'templates', 'head', ''),
+    'out_dir': os.path.join('build', 'templates', 'head', '')
+}
+
+HASHES_JS_FILENAME = 'hashes.js'
+HASHES_JS_FILEPATH = os.path.join(ASSETS_DEV_DIR, HASHES_JS_FILENAME)
 MANIFEST_FILE_PATH = os.path.join('manifest.json')
 
 REMOVE_WS = re.compile(r'\s{2,}').sub
@@ -59,19 +68,21 @@
 UGLIFY_FILE = os.path.join(
     PARENT_DIR, 'node_modules', 'uglify-js', 'bin', 'uglifyjs')
 
-FONT_EXTENSIONS = ('*.eot', '*.woff2', '*.ttf', '*.woff', '*.eof', '*.svg')
-
-# Files with this extension shouldn't be moved to build directory.
-FILE_EXTENSIONS_TO_IGNORE = ('.py',)
-
-# Files with this paths should be moved to build directory but shouldn't be
-# renamed (i.e. the filepath shouldn't contain hash)
-# This is because these files are referenced from third party files or don't
-# need cache invalidation.
+# Files with these extensions shouldn't be moved to build directory.
+FILE_EXTENSIONS_TO_IGNORE = ('.py', '.pyc', '.stylelintrc')
+# Files with these name patterns shouldn't be moved to build directory, and will
+# not be served in production. (This includes protractor.js files in
+# /extensions.)
+JS_FILENAME_SUFFIXES_TO_IGNORE = ('Spec.js', 'protractor.js')
+GENERAL_FILENAMES_TO_IGNORE = ('.pyc', '.stylelintrc')
+# These filepaths shouldn't be renamed (i.e. the filepath shouldn't contain
+# hash).
+# This is because these files don't need cache invalidation, are referenced
+# from third party files or should not be moved to the build directory.
 FILEPATHS_NOT_TO_RENAME = (
-    'backend_prod_files/third_party/generated/fonts/*',
-    'backend_prod_files/third_party/generated/js/third_party.min.js.map',
-    'backend_prod_files/third_party/generated/css/third_party.min.css.map')
+    '*.py',
+    'third_party/generated/fonts/*',
+    'third_party/generated/js/third_party.min.js.map')
 
 # Hashes for files with these paths should be provided to the frontend in
 # JS hashes object.
@@ -94,51 +105,62 @@ def _minify(source_path, target_path):
     subprocess.check_call(cmd, shell=True)
 
 
-def _join_files(source_paths, target_file_path):
-    """Writes multiple files into one file.
+def write_to_file_stream(file_stream, content):
+    """Write to a file object using provided content.
 
     Args:
-        source_paths: list(str). Paths to files to join together.
-        target_file_path: str. Path to location of the joined file.
+        file_stream: file. A stream handling object to do write operation on.
+        content: str. String content to write to file object.
     """
-    ensure_directory_exists(target_file_path)
-    with open(target_file_path, 'w+') as target_file:
-        for source_path in source_paths:
-            with open(source_path, 'r') as source_file:
-                target_file.write(source_file.read())
+    file_stream.write(content)
 
 
-def _minify_and_create_sourcemap(source_paths, target_file_path):
-    """Minifies multiple files into one file and generates source map
-    for that file.
+def _join_files(source_paths, target_file_stream):
+    """Writes multiple files into one file.
 
     Args:
-        source_paths: list(str). Paths to files to join and minify.
-        target_file_path: str. Path to location of the joined file.
+        source_paths: list(str). Paths to files to join together.
+        target_file_stream: file. A stream object of target file.
     """
+    for source_path in source_paths:
+        with open(source_path, 'r') as source_file:
+            write_to_file_stream(target_file_stream, source_file.read())
 
-    ensure_directory_exists(target_file_path)
 
+def _minify_and_create_sourcemap(source_path, target_file_path):
+    """Minifies and generates source map for a JS file. This function is only
+    meant to be used with third_party.min.js.
+
+    Args:
+        source_path: str. Path to JS file to minify.
+        target_file_path: str. Path to location of the minified file.
+    """
+    print 'Minifying and creating sourcemap for %s' % source_path
     source_map_properties = 'includeSources,url=\'third_party.min.js.map\''
     cmd = '%s %s %s -c -m --source-map %s -o %s ' % (
-        NODE_FILE, UGLIFY_FILE, ' '.join(source_paths),
+        NODE_FILE, UGLIFY_FILE, source_path,
         source_map_properties, target_file_path)
     subprocess.check_call(cmd, shell=True)
 
 
-def _copy_fonts(source_paths, target_path):
-    """Copies fonts at source paths to target path.
+def _generate_copy_tasks_for_fonts(source_paths, target_path):
+    """Queue up a copy task for each font file.
 
     Args:
         source_paths: list(str). Paths to fonts.
         target_path: str. Path where the fonts should be copied.
-    """
-    ensure_directory_exists(target_path)
 
-    for font_wildcard in source_paths:
-        font_paths = glob.glob(font_wildcard)
-        for font_path in font_paths:
-            shutil.copy(font_path, target_path)
+    Returns:
+        deque(Thread). A deque that contains all copy tasks queued
+            to be processed.
+    """
+    copy_tasks = collections.deque()
+    for font_path in source_paths:
+        copy_task = threading.Thread(
+            target=shutil.copy,
+            args=(font_path, target_path,))
+        copy_tasks.append(copy_task)
+    return copy_tasks
 
 
 def _insert_hash(filepath, file_hash):
@@ -167,6 +189,17 @@ def ensure_directory_exists(filepath):
         os.makedirs(directory)
 
 
+def safe_delete_directory_tree(directory_path):
+    """Recursively delete a directory tree. If directory tree does not exist,
+    create the directories first then delete the directory tree.
+
+    Args:
+        directory_path: str. Directory path to be deleted.
+    """
+    ensure_directory_exists(directory_path)
+    shutil.rmtree(directory_path)
+
+
 def _ensure_files_exist(filepaths):
     """Ensures that files exist at the given filepaths.
 
@@ -174,47 +207,114 @@ def _ensure_files_exist(filepaths):
         filepaths: list(str). Paths to files that we want to ensure exist.
 
     Raises:
-        Exception: Raised if one or more of the files does not exist.
+        OSError: One or more of the files does not exist.
     """
     for filepath in filepaths:
         if not os.path.isfile(filepath):
-            raise Exception('File %s does not exist.' % filepath)
+            raise OSError('File %s does not exist.' % filepath)
 
 
-def process_html(source_path, target_path, file_hashes):
-    """Copies contents of HTML file, while removing whitespace and
-    replacing paths inside the file with paths with hashes.
+def safe_copy_file(source_filepath, target_filepath):
+    """Copy a file (no metadata) after ensuring the file exists at the given
+    source filepath.
+    NOTE: shutil.copyfile does not accept directory path as arguments.
 
     Args:
-        source_path: str. Absolute path to file to be processed.
-        target_path: str. Absolute path to location where to copy
-            the processed file.
-        file_hashes: dict(str, str). Dictionary of file hashes.
+        source_filepath: str. Path to source file that we want to copy from.
+        target_filepath: str. Path to target file that we want to copy to.
+    """
+    _ensure_files_exist([source_filepath])
+    shutil.copyfile(source_filepath, target_filepath)
+
+
+def safe_delete_file(filepath):
+    """Delete a file after ensuring the provided file actually exists.
+
+    Args:
+        filepath: str. Filepath to be deleted.
+    """
+    _ensure_files_exist([filepath])
+    os.remove(filepath)
+
+
+def get_file_count(directory_path):
+    """Count total number of file in the given directory, ignoring any files
+    with extensions in FILE_EXTENSIONS_TO_IGNORE or files that should not be
+    built.
+
+    Args:
+        directory_path: str. Directory to be walked.
+
+    Returns:
+        int. Total number of files minus ignored files.
+    """
+    total_file_count = 0
+    for _, _, filenames in os.walk(directory_path):
+        for filename in filenames:
+            # Ignore files with certain extensions.
+            if should_file_be_built(filename) and not any(
+                    filename.endswith(p) for p in FILE_EXTENSIONS_TO_IGNORE):
+                total_file_count += 1
+    return total_file_count
+
+
+def _compare_file_count(first_dir_path, second_dir_path):
+    """Ensure that two dir's file counts match.
+
+    Args:
+       first_dir_path: str. First directory to compare.
+       second_dir_path: str. Second directory to compare.
+
+    Raises:
+        ValueError: The source directory does not have the same file count as
+            the target directory.
+    """
+    first_dir_file_count = get_file_count(first_dir_path)
+    second_dir_file_count = get_file_count(second_dir_path)
+    if first_dir_file_count != second_dir_file_count:
+        print 'Comparing %s vs %s' % (first_dir_path, second_dir_path)
+        raise ValueError(
+            '%s files in first dir != %s files in second dir' % (
+                first_dir_file_count, second_dir_file_count))
+
+
+def process_html(source_file_stream, target_file_stream, file_hashes):
+    """Remove whitespaces and add hashes to filepaths in the HTML file stream
+    object.
+
+    Args:
+        source_file_stream: file. The stream object of the HTML file to be
+            read from.
+        target_file_stream: file. The stream object to write the minified HTML
+            file to.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
     """
-    f = open(source_path, 'r')
-    content = f.read()
+    content = source_file_stream.read()
     for filepath, file_hash in file_hashes.iteritems():
         # We are adding hash in all file paths except for html paths.
-        # This is because html paths are used by backend and we work with paths
-        # without hash part in backend.
-        if filepath.endswith('.html'):
-            continue
-        filepath_with_hash = _insert_hash(filepath, file_hash)
-        content = content.replace(
-            '%s%s' % (TEMPLATES_DEV_DIR, filepath),
-            '%s%s' % (TEMPLATES_OUT_DIR, filepath_with_hash))
-        content = content.replace(
-            '%s%s' % (ASSETS_DEV_DIR, filepath),
-            '%s%s' % (ASSETS_OUT_DIR, filepath_with_hash))
-        content = content.replace(
-            '%s%s' % (EXTENSIONS_DEV_DIR, filepath),
-            '%s%s' % (EXTENSIONS_OUT_DIR, filepath_with_hash))
-        content = content.replace(
-            '%s%s' % (THIRD_PARTY_GENERATED_DEV_DIR, filepath),
-            '%s%s' % (THIRD_PARTY_GENERATED_OUT_DIR, filepath_with_hash))
+        # This is because html paths are used by backend and we work with
+        # paths without hash part in backend.
+        if not filepath.endswith('.html'):
+            filepath_with_hash = _insert_hash(filepath, file_hash)
+            content = content.replace(
+                '%s%s' % (TEMPLATES_DEV_DIR, filepath),
+                '%s%s' % (
+                    TEMPLATES_CORE_DIRNAMES_TO_DIRPATHS['out_dir'],
+                    filepath_with_hash))
+            content = content.replace(
+                '%s%s' % (ASSETS_DEV_DIR, filepath),
+                '%s%s' % (ASSETS_OUT_DIR, filepath_with_hash))
+            content = content.replace(
+                '%s%s' % (EXTENSIONS_DIRNAMES_TO_DIRPATHS['dev_dir'], filepath),
+                '%s%s' % (
+                    EXTENSIONS_DIRNAMES_TO_DIRPATHS['out_dir'],
+                    filepath_with_hash))
+            content = content.replace(
+                '%s%s' % (THIRD_PARTY_GENERATED_DEV_DIR, filepath),
+                '%s%s' % (THIRD_PARTY_GENERATED_OUT_DIR, filepath_with_hash))
     content = REMOVE_WS(' ', content)
-    d = open(target_path, 'w+')
-    d.write(content)
+    write_to_file_stream(target_file_stream, content)
 
 
 def get_dependency_directory(dependency):
@@ -285,10 +385,19 @@ def get_font_filepaths(dependency_bundle, dependency_dir):
         list(str). List of paths to font files that need to be copied.
     """
     if 'fontsPath' not in dependency_bundle:
+        # Skip dependency bundles in manifest.json that do not have
+        # fontsPath property.
         return []
     fonts_path = dependency_bundle['fontsPath']
-    return [os.path.join(dependency_dir, fonts_path, extension)
-            for extension in FONT_EXTENSIONS]
+    # Obtain directory path to /font inside dependency folder.
+    # E.g. third_party/static/bootstrap-3.3.4/fonts/.
+    font_dir = os.path.join(dependency_dir, fonts_path)
+    font_filepaths = []
+    # Walk the directory and add all font files to list.
+    for root, _, filenames in os.walk(font_dir):
+        for filename in filenames:
+            font_filepaths.append(os.path.join(root, filename))
+    return font_filepaths
 
 
 def get_dependencies_filepaths():
@@ -297,9 +406,9 @@ def get_dependencies_filepaths():
 
     Returns:
         dict(str, list(str)). A dict mapping file types to lists of filepaths.
-        The dict has three keys: 'js', 'css' and 'fonts'. Each of the
-        corresponding values is a full list of dependency file paths of the
-        given type.
+            The dict has three keys: 'js', 'css' and 'fonts'. Each of the
+            corresponding values is a full list of dependency file paths of the
+            given type.
     """
     filepaths = {
         'js': [],
@@ -322,48 +431,58 @@ def get_dependencies_filepaths():
 
     _ensure_files_exist(filepaths['js'])
     _ensure_files_exist(filepaths['css'])
-
+    _ensure_files_exist(filepaths['fonts'])
     return filepaths
 
 
-def build_minified_third_party_libs():
-    """Joins all third party css files into single css file and js files into
-    single js file. Minifies both files. Copies both files and all fonts into
-    third party folder.
+def minify_third_party_libs(third_party_directory_path):
+    """Minify third_party.js and third_party.css and remove un-minified files.
     """
+    THIRD_PARTY_JS_FILEPATH = os.path.join(
+        third_party_directory_path, THIRD_PARTY_JS_RELATIVE_FILEPATH)
+    THIRD_PARTY_CSS_FILEPATH = os.path.join(
+        third_party_directory_path, THIRD_PARTY_CSS_RELATIVE_FILEPATH)
 
-    print 'Building minified third party libs...'
+    MINIFIED_THIRD_PARTY_JS_FILEPATH = os.path.join(
+        third_party_directory_path, MINIFIED_THIRD_PARTY_JS_RELATIVE_FILEPATH)
+    MINIFIED_THIRD_PARTY_CSS_FILEPATH = os.path.join(
+        third_party_directory_path, MINIFIED_THIRD_PARTY_CSS_RELATIVE_FILEPATH)
 
-    third_party_js = os.path.join(
-        THIRD_PARTY_GENERATED_STAGING_DIR, 'js', 'third_party.min.js')
-    third_party_css = os.path.join(
-        THIRD_PARTY_GENERATED_STAGING_DIR, 'css', 'third_party.min.css')
-    fonts_dir = os.path.join(THIRD_PARTY_GENERATED_STAGING_DIR, 'fonts', '')
+    _minify_and_create_sourcemap(
+        THIRD_PARTY_JS_FILEPATH, MINIFIED_THIRD_PARTY_JS_FILEPATH)
+    _minify(THIRD_PARTY_CSS_FILEPATH, MINIFIED_THIRD_PARTY_CSS_FILEPATH)
+    # Clean up un-minified third_party.js and third_party.css.
+    safe_delete_file(THIRD_PARTY_JS_FILEPATH)
+    safe_delete_file(THIRD_PARTY_CSS_FILEPATH)
 
-    dependency_filepaths = get_dependencies_filepaths()
-    _minify_and_create_sourcemap(dependency_filepaths['js'], third_party_js)
-    _join_files(dependency_filepaths['css'], third_party_css)
-    _minify(third_party_css, third_party_css)
-    _copy_fonts(dependency_filepaths['fonts'], fonts_dir)
 
-
-def build_third_party_libs():
+def build_third_party_libs(third_party_directory_path):
     """Joins all third party css files into single css file and js files into
     single js file. Copies both files and all fonts into third party folder.
     """
 
-    print 'Building third party libs...'
+    print 'Building third party libs at %s' % third_party_directory_path
 
-    third_party_js = os.path.join(
-        THIRD_PARTY_GENERATED_DEV_DIR, 'js', 'third_party.js')
-    third_party_css = os.path.join(
-        THIRD_PARTY_GENERATED_DEV_DIR, 'css', 'third_party.css')
-    fonts_dir = os.path.join(THIRD_PARTY_GENERATED_DEV_DIR, 'fonts', '')
+    THIRD_PARTY_JS_FILEPATH = os.path.join(
+        third_party_directory_path, THIRD_PARTY_JS_RELATIVE_FILEPATH)
+    THIRD_PARTY_CSS_FILEPATH = os.path.join(
+        third_party_directory_path, THIRD_PARTY_CSS_RELATIVE_FILEPATH)
+    FONTS_DIR = os.path.join(
+        third_party_directory_path, FONTS_RELATIVE_DIRECTORY_PATH)
 
     dependency_filepaths = get_dependencies_filepaths()
-    _join_files(dependency_filepaths['js'], third_party_js)
-    _join_files(dependency_filepaths['css'], third_party_css)
-    _copy_fonts(dependency_filepaths['fonts'], fonts_dir)
+    ensure_directory_exists(THIRD_PARTY_JS_FILEPATH)
+    with open(THIRD_PARTY_JS_FILEPATH, 'w+') as third_party_js_file:
+        _join_files(dependency_filepaths['js'], third_party_js_file)
+
+    ensure_directory_exists(THIRD_PARTY_CSS_FILEPATH)
+    with open(THIRD_PARTY_CSS_FILEPATH, 'w+') as third_party_css_file:
+        _join_files(dependency_filepaths['css'], third_party_css_file)
+
+    ensure_directory_exists(FONTS_DIR)
+    _execute_tasks(
+        _generate_copy_tasks_for_fonts(
+            dependency_filepaths['fonts'], FONTS_DIR))
 
 
 def hash_should_be_inserted(filepath):
@@ -380,46 +499,73 @@ def hash_should_be_inserted(filepath):
                    in FILEPATHS_NOT_TO_RENAME)
 
 
-def copy_files_source_to_target(source, target, file_hashes):
-    """Copies all files in source directory to target directory, renames
-    them to include hash in their name.
+def should_file_be_built(filepath):
+    """Determines if the file should be built.
+        - JS files: Returns False if filepath matches with pattern in
+        JS_FILENAME_SUFFIXES_TO_IGNORE, else returns True.
+        - Python files: Returns False if filepath ends with _test.py, else
+        returns True
+        - Other files: Returns False if filepath matches with pattern in
+        GENERAL_FILENAMES_TO_IGNORE, else returns True.
+
+    Args:
+        filepath: str. Path relative to file we are currently building.
+
+    Returns:
+        bool. True if filepath should be built, else False.
+    """
+    if filepath.endswith('.js'):
+        return not any(
+            filepath.endswith(p) for p in JS_FILENAME_SUFFIXES_TO_IGNORE)
+    elif filepath.endswith('_test.py'):
+        return False
+    else:
+        return not any(
+            filepath.endswith(p) for p in GENERAL_FILENAMES_TO_IGNORE)
+
+
+def generate_copy_tasks_to_copy_from_source_to_target(
+        source, target, file_hashes):
+    """Generate copy task for each file in source directory, excluding files
+    with extensions in FILE_EXTENSIONS_TO_IGNORE. Insert hash from hash dict
+    into the destination filename.
 
     Args:
         source: str. Path relative to /oppia directory of directory
             containing files and directories to be copied.
         target: str. Path relative to /oppia directory of directory where
             to copy the files and directories.
-        file_hashes: dict(str, str). Dictionary of file hashes.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
+
+    Returns:
+        deque(Thread). A deque that contains all copy tasks queued
+            to be processed.
     """
     print 'Processing %s' % os.path.join(os.getcwd(), source)
     print 'Copying into %s' % os.path.join(os.getcwd(), target)
-
-    ensure_directory_exists(target)
-    shutil.rmtree(target)
-    for root, dirs, files in os.walk(os.path.join(os.getcwd(), source)):
-        for directory in dirs:
+    copy_tasks = collections.deque()
+    for root, dirnames, filenames in os.walk(os.path.join(os.getcwd(), source)):
+        for directory in dirnames:
             print 'Copying %s' % os.path.join(root, directory)
-
-        for filename in files:
+        for filename in filenames:
             source_path = os.path.join(root, filename)
-            if target in source_path:
-                continue
-            if source not in source_path:
-                continue
-
-            # Ignore files with certain extensions.
-            if any(source_path.endswith(p) for p in FILE_EXTENSIONS_TO_IGNORE):
-                continue
-
-            target_path = source_path
-            relative_path = os.path.relpath(source_path, source)
-            if hash_should_be_inserted(source + relative_path):
-                relative_path = (
-                    _insert_hash(relative_path, file_hashes[relative_path]))
-
-            target_path = os.path.join(os.getcwd(), target, relative_path)
-            ensure_directory_exists(target_path)
-            shutil.copyfile(source_path, target_path)
+            # Python files should not be copied to final build directory.
+            if not any(
+                    source_path.endswith(p) for p in FILE_EXTENSIONS_TO_IGNORE):
+                target_path = source_path
+                relative_path = os.path.relpath(source_path, source)
+                if hash_should_be_inserted(source + relative_path):
+                    relative_path = (
+                        _insert_hash(relative_path, file_hashes[relative_path]))
+
+                target_path = os.path.join(os.getcwd(), target, relative_path)
+                ensure_directory_exists(target_path)
+                copy_task = threading.Thread(
+                    target=safe_copy_file,
+                    args=(source_path, target_path,))
+                copy_tasks.append(copy_task)
+    return copy_tasks
 
 
 def is_file_hash_provided_to_frontend(filepath):
@@ -454,42 +600,72 @@ def generate_md5_hash(filepath):
     return m.hexdigest()
 
 
+def get_filepaths_by_extensions(source_dir, file_extensions):
+    """Return list of filepaths in a directory with certain extensions,
+    excluding filepaths that should not be built.
+
+    Args:
+        source_dir: str. Root directory to be walked.
+        file_extensions: tuple(str). Tuple of file extensions.
+
+    Returns:
+        list(str). List of filepaths with specified extensions.
+    """
+    filepaths = []
+    for root, _, filenames in os.walk(source_dir):
+        for filename in filenames:
+            filepath = os.path.join(root, filename)
+            relative_filepath = os.path.relpath(filepath, source_dir)
+            if should_file_be_built(filename) and any(
+                    filename.endswith(p) for p in file_extensions):
+                filepaths.append(relative_filepath)
+    return filepaths
+
+
 def get_file_hashes(directory_path):
-    """Returns hashes of all files in directory tree.
+    """Returns hashes of all files in directory tree, excluding files with
+    extensions in FILE_EXTENSIONS_TO_IGNORE or files that should not be built.
 
     Args:
         directory_path: str. Root directory of the tree.
 
     Returns:
         dict(str, str). Dictionary with keys specifying file paths and values
-        specifying file hashes.
+            specifying file hashes.
     """
     file_hashes = dict()
 
     print('Computing hashes for files in %s'
           % os.path.join(os.getcwd(), directory_path))
 
-    for root, dirs, files in os.walk(os.path.join(os.getcwd(), directory_path)):
-        for directory in dirs:
+    for root, dirnames, filenames in os.walk(
+            os.path.join(os.getcwd(), directory_path)):
+        for directory in dirnames:
             print('Computing hashes for files in %s'
                   % os.path.join(root, directory))
-        for filename in files:
-            filepath = os.path.join(root, filename)
-            relative_filepath = os.path.relpath(filepath, directory_path)
-            file_hashes[relative_filepath] = generate_md5_hash(filepath)
+        for filename in filenames:
+            if should_file_be_built(filename) and not any(
+                    filename.endswith(p) for p in FILE_EXTENSIONS_TO_IGNORE):
+                complete_filepath = os.path.join(root, filename)
+                relative_filepath = os.path.relpath(
+                    complete_filepath, directory_path)
+                file_hashes[relative_filepath] = generate_md5_hash(
+                    complete_filepath)
 
     return file_hashes
 
 
 def filter_hashes(file_hashes):
     """Filters hashes that should be provided to the frontend
-    and prefixes / in front of the keys.
+    and prefixes "/" in front of the keys.
 
     Args:
-        file_hashes: dict(str, str). Dictionary of file hashes.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
 
     Returns:
-        dict(str, str). Filtered dictionary of file hashes.
+        dict(str, str). Filtered dictionary of only filepaths that should be
+            provided to the frontend.
     """
     filtered_hashes = dict()
     for filepath, file_hash in file_hashes.iteritems():
@@ -502,7 +678,8 @@ def get_hashes_json_file_contents(file_hashes):
     """Return JS code that loads hashes needed for frontend into variable.
 
     Args:
-        file_hashes: dict(str, str). Dictionary of file hashes.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
 
     Returns:
         str. JS code loading hashes as JSON into variable.
@@ -514,37 +691,25 @@ def get_hashes_json_file_contents(file_hashes):
     return 'var hashes = JSON.parse(\'%s\');' % (hashes_json)
 
 
-def save_hashes_as_json(target_filepath, file_hashes):
-    """Save hashes in JS file containing JSON for files that
-    are to be interpolated in the frontend.
-
-    Args:
-        target_filepath: str. Path relative to /oppia directory defining
-            location where to save the JS file with hashes. The final location
-            path would also contain hash.
-        file_hashes: dict(str, str). Dictionary of file hashes.
-    """
-
-    with open(target_filepath, 'w') as f:
-        f.write(get_hashes_json_file_contents(file_hashes))
-
-    file_hash = generate_md5_hash(target_filepath)
-    relative_filepath = os.path.relpath(
-        target_filepath, os.path.join(os.path.curdir, 'build', 'assets'))
-    filepath_with_hash = _insert_hash(target_filepath, file_hash)
-    os.rename(target_filepath, filepath_with_hash)
-    file_hashes[relative_filepath] = file_hash
-
-
 def minify_func(source_path, target_path, file_hashes, filename):
+    """Call the appropriate functions to handle different types of file
+    formats:
+        - HTML files: Remove whitespaces, interpolates paths in HTML to include
+        hashes in source directory and save edited file at target directory.
+        - CSS or JS files: Minify and save at target directory.
+        - Other files: Copy the file from source directory to target directory.
+    """
     if filename.endswith('.html'):
-        process_html(source_path, target_path, file_hashes)
-    elif filename.endswith('.css'):
-        _minify(source_path, target_path)
-    elif filename.endswith('.js'):
+        print 'Building %s' % source_path
+        with open(source_path, 'r+') as source_html_file:
+            with open(target_path, 'w+') as minified_html_file:
+                process_html(source_html_file, minified_html_file, file_hashes)
+    elif filename.endswith('.css') or filename.endswith('.js'):
+        print 'Minifying %s' % source_path
         _minify(source_path, target_path)
     else:
-        shutil.copyfile(source_path, target_path)
+        print 'Copying %s' % source_path
+        safe_copy_file(source_path, target_path)
 
 
 def _execute_tasks(tasks, batch_size=24):
@@ -552,130 +717,140 @@ def _execute_tasks(tasks, batch_size=24):
 
     Runs no more than 'batch_size' tasks at a time.
     """
-    remaining_tasks = list(tasks)
+    remaining_tasks = collections.deque(tasks)
     currently_running_tasks = []
 
     while remaining_tasks or currently_running_tasks:
         if currently_running_tasks:
-            for task in list(currently_running_tasks):
+            for task in collections.deque(currently_running_tasks):
                 if not task.is_alive():
                     currently_running_tasks.remove(task)
-
         while remaining_tasks and len(currently_running_tasks) < batch_size:
-            task = remaining_tasks.pop()
+            task = remaining_tasks.popleft()
             currently_running_tasks.append(task)
-            task.start()
+            try:
+                task.start()
+            except RuntimeError as threadAlreadyStarted:
+                raise OSError(threadAlreadyStarted.message)
 
 
-def build_files(source, target, file_hashes, file_formats=None):
-    """If no specific file formats is provided, minifies all CSS and JS files,
-    removes whitespace from HTML and interpolates paths in HTML to include
-    hashes in source directory and copies it to target.
-    If specific file formats is provided, only files pertaining to the provided
-    formats will be minified.
+def generate_build_tasks_to_build_all_files_in_directory(
+        source, target, file_hashes):
+    """This function queues up tasks to build all files in a directory,
+    excluding files that should not be built.
 
     Args:
-        source: str. Path relative to /oppia directory of directory
-            containing files and directories to be copied.
-        target: str. Path relative to /oppia directory of directory where
-            to copy the files and directories.
-        file_hashes: dict(str, str). Dictionary of file hashes.
-        file_formats: tuple(str) or None. Tuple of specific file formats to be
-            built. If None then all files within the source directory will be
-            built.
+        source: str.  Path relative to /oppia of directory containing source
+            files and directories to be built.
+        target: str. Path relative to /oppia of directory where the built files
+            and directories will be saved to.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
+
+    Returns:
+        deque(Thread). A deque that contains all build tasks queued
+            to be processed.
     """
     print 'Processing %s' % os.path.join(os.getcwd(), source)
     print 'Generating into %s' % os.path.join(os.getcwd(), target)
-    ensure_directory_exists(target)
-    if file_formats is None:
-        print 'Deleting dir %s' % target
-        # Only delete BUILD directory when building all files.
-        shutil.rmtree(target)
-
-    for root, dirs, files in os.walk(os.path.join(os.getcwd(), source)):
-        tasks = []
-        for directory in dirs:
+    build_tasks = collections.deque()
+
+    for root, dirnames, filenames in os.walk(os.path.join(os.getcwd(), source)):
+        for directory in dirnames:
             print 'Building directory %s' % os.path.join(root, directory)
-        for filename in files:
+        for filename in filenames:
             source_path = os.path.join(root, filename)
-            if target in source_path:
-                continue
-            if source not in source_path:
-                continue
             target_path = source_path.replace(source, target)
             ensure_directory_exists(target_path)
-            if file_formats is None or any(
-                    filename.endswith(p) for p in file_formats):
+            if should_file_be_built(source_path):
                 task = threading.Thread(
                     target=minify_func,
-                    args=(source_path, target_path, file_hashes, filename))
-            else:
-                # Skip files that are not the specified format.
-                continue
-            tasks.append(task)
-        try:
-            _execute_tasks(tasks)
-        except Exception as e:
-            print e
+                    args=(source_path, target_path, file_hashes, filename,))
+                build_tasks.append(task)
+    return build_tasks
 
 
-def rebuild_new_files(
-        source_path, target_path, recently_changed_filenames, file_hashes):
-    """Minify recently changed files.
+def generate_build_tasks_to_build_files_from_filepaths(
+        source_path, target_path, filepaths, file_hashes):
+    """This function queues up build tasks to build files from a list of
+    filepaths, excluding files that should not be built.
 
     Args:
         source_path: str. Path relative to /oppia directory of directory
             containing files and directories to be copied.
         target_path: str. Path relative to /oppia directory of directory where
             to copy the files and directories.
-        recently_changed_filenames: list(str). List of filenames that were
-            recently changed.
-        file_hashes: dict(str, str). Dictionary of file hashes.
-    """
-    for file_name in recently_changed_filenames:
-        print 'Minifying %s' % file_name
-        source_file_path = os.path.join(source_path, file_name)
-        target_file_path = os.path.join(target_path, file_name)
-        ensure_directory_exists(target_file_path)
-        minify_func(source_file_path, target_file_path, file_hashes, file_name)
+        filepaths: list(str). List of filepaths to be built.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
 
-
-def remove_deleted_files(dev_directory, staging_directory):
-    """Walk the staging directory and remove files that have since been removed
-    from the DEV directory.
+    Returns:
+        deque(Thread). A deque that contains all build tasks queued
+            to be processed.
+    """
+    build_tasks = collections.deque()
+    for filepath in filepaths:
+        source_file_path = os.path.join(source_path, filepath)
+        target_file_path = os.path.join(target_path, filepath)
+        ensure_directory_exists(target_file_path)
+        if should_file_be_built(source_path):
+            task = threading.Thread(
+                target=minify_func,
+                args=(
+                    source_file_path, target_file_path, file_hashes, filepath,))
+            build_tasks.append(task)
+    return build_tasks
+
+
+def generate_delete_tasks_to_remove_deleted_files(
+        source_dir_hashes, staging_directory):
+    """This function walks the staging directory and queues up deletion tasks to
+    remove files that are not in the hash dict i.e. remaining files in staging
+    directory that have since been deleted from source directory. Files with
+    extensions in FILE_EXTENSIONS_TO_IGNORE will be excluded.
 
     Args:
-        dev_directory: str. Path relative to /oppia where DEV files are located.
+        source_dir_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
         staging_directory: str. Path relative to /oppia directory of directory
             containing files and directories to be walked.
+
+    Returns:
+        deque(Thread). A deque that contains all delete tasks
+            queued to be processed.
     """
-    file_hashes = get_file_hashes(dev_directory)
     print 'Scanning directory %s to remove deleted file' % staging_directory
-    for root, dirs, files in os.walk(
+    delete_tasks = collections.deque()
+    for root, dirnames, filenames in os.walk(
             os.path.join(os.getcwd(), staging_directory)):
-        for directory in dirs:
+        for directory in dirnames:
             print 'Scanning %s' % os.path.join(root, directory)
-        for filename in files:
+        for filename in filenames:
             target_path = os.path.join(root, filename)
             # Ignore files with certain extensions.
-            if any(target_path.endswith(p) for p in FILE_EXTENSIONS_TO_IGNORE):
-                continue
-            relative_path = os.path.relpath(target_path, staging_directory)
-            # Remove file found in staging dir but not in /DEV, i.e.
-            # file not listed in hash dict.
-            if relative_path not in file_hashes:
-                print ('Unable to find %s in file hashes, deleting file'
-                       % relative_path)
-                os.remove(target_path)
-
-
-def get_recently_changed_filenames(dev_dir, out_dir):
-    """Compare hashes of DEV files and BUILD files. Return a list of filenames
-    that were recently changed.
+            if not any(
+                    target_path.endswith(p) for p in FILE_EXTENSIONS_TO_IGNORE):
+                relative_path = os.path.relpath(target_path, staging_directory)
+                # Remove file found in staging directory but not in source
+                # directory, i.e. file not listed in hash dict.
+                if relative_path not in source_dir_hashes:
+                    print ('Unable to find %s in file hashes, deleting file'
+                           % target_path)
+                    task = threading.Thread(
+                        target=safe_delete_file, args=(target_path,))
+                    delete_tasks.append(task)
+    return delete_tasks
+
+
+def get_recently_changed_filenames(source_dir_hashes, out_dir):
+    """Compare hashes of source files and built files. Return a list of
+    filenames that were recently changed. Skips files that are not supposed to
+    built or already built.
 
     Args:
-        dev_dir: str. Path relative to /oppia where DEV files are located.
-        out_dir: str. Path relative to /oppia where BUILD files are located.
+        source_dir_hashes: dict(str, str). Dictionary of hashes of files
+            to be built.
+        out_dir: str. Path relative to /oppia where built files are located.
 
     Returns:
         list(str). List of filenames expected to be re-hashed.
@@ -684,26 +859,177 @@ def get_recently_changed_filenames(dev_dir, out_dir):
     # the filenames and their extensions,
     # e.g base.240933e7564bd72a4dde42ee23260c5f.html
     # If a file gets edited, a different MD5 hash is generated.
-    file_hashes = get_file_hashes(dev_dir)
     recently_changed_filenames = []
-    # Currently, we do not hash Python files and HTML files are always re-built.
+    # Currently, Python files and HTML files are always re-built.
     FILE_EXTENSIONS_NOT_TO_TRACK = ('.html', '.py',)
-    print "Comparing file hashes of %s and %s" % (dev_dir, out_dir)
-    for file_name, md5_hash in file_hashes.iteritems():
-        # Ignore files with certain extensions.
-        if any(file_name.endswith(p) for p in FILE_EXTENSIONS_NOT_TO_TRACK):
-            continue
-        final_filepath = _insert_hash(
-            os.path.join(out_dir, file_name), md5_hash)
-        if not os.path.isfile(final_filepath):
-            # Filename with provided hash cannot be found, this file has been
-            # recently changed or created since last build.
-            recently_changed_filenames.append(file_name)
-    print ('The following files will be rebuilt due to recent changes: %s' %
-           recently_changed_filenames)
+    for filename, md5_hash in source_dir_hashes.iteritems():
+        # Skip files that are already built or should not be built.
+        if should_file_be_built(filename) and not any(
+                filename.endswith(p) for p in FILE_EXTENSIONS_NOT_TO_TRACK):
+            final_filepath = _insert_hash(
+                os.path.join(out_dir, filename), md5_hash)
+            if not os.path.isfile(final_filepath):
+                # Filename with provided hash cannot be found, this file has
+                # been recently changed or created since last build.
+                recently_changed_filenames.append(filename)
+    if recently_changed_filenames:
+        print ('The following files will be rebuilt due to recent changes: %s' %
+               recently_changed_filenames)
     return recently_changed_filenames
 
 
+def generate_build_tasks_to_build_directory(dirnames_dict, file_hashes):
+    """This function queues up build tasks to build all files in source
+    directory if there is no existing staging directory. Otherwise, selectively
+    queue up build tasks to build recently changed files.
+
+    Args:
+        dirnames_dict: dict(str, str). This dict should contain three keys,
+            with corresponding values as follows:
+            - 'dev_dir': the directory that contains source files to be built.
+            - 'staging_dir': the directory that contains minified files waiting
+            for final copy process.
+            - 'out_dir': the final directory that contains built files with hash
+            inserted into filenames.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
+
+    Returns:
+        deque(Thread). A deque that contains all build tasks queued
+            to be processed.
+    """
+    source_dir = dirnames_dict['dev_dir']
+    staging_dir = dirnames_dict['staging_dir']
+    out_dir = dirnames_dict['out_dir']
+    build_tasks = collections.deque()
+    if not os.path.isdir(staging_dir):
+        # If there is no staging dir, perform build process on all files.
+        print 'Creating new %s folder' % staging_dir
+        ensure_directory_exists(staging_dir)
+        build_tasks += generate_build_tasks_to_build_all_files_in_directory(
+            source_dir, staging_dir, file_hashes)
+    else:
+        # If staging dir exists, rebuild all HTML and Python files.
+        file_extensions_to_always_rebuild = ('.html', '.py',)
+        print (
+            'Staging dir exists, re-building all %s files'
+            % str(file_extensions_to_always_rebuild))
+
+        filenames_to_always_rebuild = get_filepaths_by_extensions(
+            source_dir, file_extensions_to_always_rebuild)
+        build_tasks += generate_build_tasks_to_build_files_from_filepaths(
+            source_dir, staging_dir, filenames_to_always_rebuild, file_hashes)
+
+        dev_dir_hashes = get_file_hashes(source_dir)
+        # Clean up files in staging directory that cannot be found in file
+        # hashes dictionary.
+        _execute_tasks(generate_delete_tasks_to_remove_deleted_files(
+            dev_dir_hashes, staging_dir))
+
+        print 'Getting files that have changed between %s and %s' % (
+            source_dir, out_dir)
+        recently_changed_filenames = get_recently_changed_filenames(
+            dev_dir_hashes, out_dir)
+        if recently_changed_filenames:
+            print 'Re-building recently changed files at %s' % source_dir
+            build_tasks += generate_build_tasks_to_build_files_from_filepaths(
+                source_dir, staging_dir, recently_changed_filenames,
+                file_hashes)
+        else:
+            print 'No changes detected. Using previously built files.'
+
+    return build_tasks
+
+
+def _verify_filepath_hash(relative_filepath, file_hashes):
+    """Ensure that hashes in filepaths match with the hash entries in hash
+    dict.
+
+    Args:
+        relative_filepath: str. Filepath that is relative from /build.
+        file_hashes: dict(str, str). Dictionary with filepaths as keys and
+            hashes of file content as values.
+
+    Raises:
+        ValueError: The hash dict is empty.
+        ValueError: Filepath has less than 2 partitions after splitting by '.'
+            delimiter.
+        ValueError: The filename does not contain hash.
+        KeyError: The filename's hash cannot be found in the hash dict.
+    """
+    # Final filepath example:
+    # head/pages/base.240933e7564bd72a4dde42ee23260c5f.html.
+    if not file_hashes:
+        raise ValueError('Hash dict is empty')
+
+    filename_partitions = relative_filepath.split('.')
+    if len(filename_partitions) < 2:
+        raise ValueError('Filepath has less than 2 partitions after splitting')
+
+    hash_string_from_filename = filename_partitions[-2]
+    # Ensure hash string obtained from filename follows MD5 hash format.
+    if not re.search(r'([a-fA-F\d]{32})', relative_filepath):
+        raise ValueError(
+            '%s is expected to contain MD5 hash' % relative_filepath)
+    if hash_string_from_filename not in file_hashes.values():
+        raise KeyError(
+            'Hash from file named %s does not match hash dict values' %
+            relative_filepath)
+
+
+def _verify_build(input_dirnames, output_dirnames, file_hashes):
+    """Verify a few metrics after build process finishes:
+        1) Number of files between source directory and final directory
+        matches.
+        2) The hashes in filenames belongs to the hash dict.
+        3) hashes.js, third_party.min.css and third_party.min.js are built and
+        hashes are inserted.
+
+        Args:
+            input_dirnames: list(str). List of directory paths that contain
+                source files.
+            output_dirnames: list(str). List of directory paths that contain
+                built files.
+            file_hashes: dict(str, str). Dictionary with filepaths as keys and
+                hashes of file content as values.
+    """
+    for i in xrange(len(input_dirnames)):
+        # Make sure that all files in source directory and staging directory are
+        # accounted for.
+        _compare_file_count(input_dirnames[i], output_dirnames[i])
+
+    # Make sure that hashed file name matches with current hash dict.
+    for built_dir in output_dirnames:
+        for root, _, filenames in os.walk(built_dir):
+            for filename in filenames:
+                parent_dir = os.path.basename(root)
+                converted_filepath = os.path.join(
+                    THIRD_PARTY_GENERATED_DEV_DIR, parent_dir, filename)
+                if hash_should_be_inserted(converted_filepath):
+                    # Obtain the same filepath format as the hash dict's key.
+                    relative_filepath = os.path.relpath(
+                        os.path.join(root, filename), built_dir)
+                    _verify_filepath_hash(relative_filepath, file_hashes)
+
+    hash_final_filename = _insert_hash(
+        HASHES_JS_FILENAME, file_hashes[HASHES_JS_FILENAME])
+
+    third_party_js_final_filename = _insert_hash(
+        MINIFIED_THIRD_PARTY_JS_RELATIVE_FILEPATH,
+        file_hashes[MINIFIED_THIRD_PARTY_JS_RELATIVE_FILEPATH])
+
+    third_party_css_final_filename = _insert_hash(
+        MINIFIED_THIRD_PARTY_CSS_RELATIVE_FILEPATH,
+        file_hashes[MINIFIED_THIRD_PARTY_CSS_RELATIVE_FILEPATH])
+
+    _ensure_files_exist([
+        os.path.join(ASSETS_OUT_DIR, hash_final_filename),
+        os.path.join(
+            THIRD_PARTY_GENERATED_OUT_DIR, third_party_js_final_filename),
+        os.path.join(
+            THIRD_PARTY_GENERATED_OUT_DIR, third_party_css_final_filename)])
+
+
 def generate_build_directory():
     """Generates hashes for files. Minifies files and interpolates paths
     in HTMLs to include hashes. Renames the files to include hashes and copies
@@ -715,63 +1041,60 @@ def generate_build_directory():
     # /build folder. This is so that the replacing inside the HTML files works
     # correctly.
     hashes = dict()
-
-    # Create hashes for assets, copy directories and files to build/assets.
-    hashes.update(get_file_hashes(ASSETS_DEV_DIR))
-    copy_files_source_to_target(ASSETS_DEV_DIR, ASSETS_OUT_DIR, hashes)
-
-    # Process third_party resources, create hashes for them and copy them into
-    # build/third_party/generated.
-    build_minified_third_party_libs()
-    hashes.update(get_file_hashes(THIRD_PARTY_GENERATED_STAGING_DIR))
-    copy_files_source_to_target(
-        THIRD_PARTY_GENERATED_STAGING_DIR,
-        THIRD_PARTY_GENERATED_OUT_DIR, hashes)
-
-    # Minify extension static resources, create hashes for them and copy them
-    # into build/extensions.
-    hashes.update(get_file_hashes(EXTENSIONS_DEV_DIR))
-    build_files(
-        EXTENSIONS_DEV_DIR, EXTENSIONS_STAGING_DIR, hashes)
-    copy_files_source_to_target(
-        EXTENSIONS_STAGING_DIR, EXTENSIONS_OUT_DIR, hashes)
-
-    # Create hashes for all template files.
-    hashes.update(get_file_hashes(TEMPLATES_DEV_DIR_CORE))
-
+    build_tasks = collections.deque()
+    copy_tasks = collections.deque()
+    # Minify third party resources.
+    minify_third_party_libs(THIRD_PARTY_GENERATED_DEV_DIR)
+
+    # Create hashes for all directories and files.
+    HASH_DIRS = [
+        ASSETS_DEV_DIR, EXTENSIONS_DIRNAMES_TO_DIRPATHS['dev_dir'],
+        TEMPLATES_CORE_DIRNAMES_TO_DIRPATHS['dev_dir'],
+        THIRD_PARTY_GENERATED_DEV_DIR]
+    for HASH_DIR in HASH_DIRS:
+        hashes.update(get_file_hashes(HASH_DIR))
     # Save hashes as JSON and write the JSON into JS file
     # to make the hashes available to the frontend.
-    save_hashes_as_json(HASHES_JSON, hashes)
-
-    # Minify all template files copy them into build/templates/head.
-    if not os.path.isdir(TEMPLATES_STAGING_DIR):
-        # If there is no staging dir, perform build process on all files.
-        print 'Creating new %s folder' % TEMPLATES_STAGING_DIR
-        build_files(
-            TEMPLATES_DEV_DIR_CORE, TEMPLATES_STAGING_DIR, hashes)
-    else:
-        # If there is an existing staging dir, rebuild all HTML files.
-        print 'Staging directory exists, re-building all HTML files'
-        build_files(
-            TEMPLATES_DEV_DIR_CORE, TEMPLATES_STAGING_DIR, hashes,
-            file_formats=('.html',))
-        recently_changed_filenames = get_recently_changed_filenames(
-            TEMPLATES_DEV_DIR_CORE, TEMPLATES_OUT_DIR)
-        if recently_changed_filenames:
-            # Only re-build files that have changed since last build.
-            print ("Re-building recently changed files at %s"
-                   % TEMPLATES_DEV_DIR_CORE)
-            rebuild_new_files(
-                TEMPLATES_DEV_DIR_CORE, TEMPLATES_STAGING_DIR,
-                recently_changed_filenames, hashes)
-            # Clean up files in staging dir that have been removed in DEV dir.
-            remove_deleted_files(TEMPLATES_DEV_DIR_CORE, TEMPLATES_STAGING_DIR)
-        else:
-            print 'No changes detected. Using previously built files.'
-    # Copy built files from staging dir to build dir.
-    copy_files_source_to_target(
-        TEMPLATES_STAGING_DIR, TEMPLATES_OUT_DIR, hashes)
-
+    ensure_directory_exists(HASHES_JS_FILEPATH)
+    with open(HASHES_JS_FILEPATH, 'w+') as hashes_js_file:
+        write_to_file_stream(
+            hashes_js_file, get_hashes_json_file_contents(hashes))
+    # Update hash dict with newly created hashes.js.
+    hashes.update({HASHES_JS_FILENAME: generate_md5_hash(HASHES_JS_FILEPATH)})
+    # Make sure /assets/hashes.js is available to the frontend.
+    _ensure_files_exist([HASHES_JS_FILEPATH])
+
+    # Build files in /extensions and copy them into staging directory.
+    build_tasks += generate_build_tasks_to_build_directory(
+        EXTENSIONS_DIRNAMES_TO_DIRPATHS, hashes)
+    # Minify all template files and copy them into staging directory.
+    build_tasks += generate_build_tasks_to_build_directory(
+        TEMPLATES_CORE_DIRNAMES_TO_DIRPATHS, hashes)
+    _execute_tasks(build_tasks)
+
+    # Copy all files from staging directory to production directory.
+    COPY_INPUT_DIRS = [
+        ASSETS_DEV_DIR, EXTENSIONS_DIRNAMES_TO_DIRPATHS['staging_dir'],
+        TEMPLATES_CORE_DIRNAMES_TO_DIRPATHS['staging_dir'],
+        THIRD_PARTY_GENERATED_DEV_DIR]
+    COPY_OUTPUT_DIRS = [
+        ASSETS_OUT_DIR, EXTENSIONS_DIRNAMES_TO_DIRPATHS['out_dir'],
+        TEMPLATES_CORE_DIRNAMES_TO_DIRPATHS['out_dir'],
+        THIRD_PARTY_GENERATED_OUT_DIR]
+    assert len(COPY_INPUT_DIRS) == len(COPY_OUTPUT_DIRS)
+    for i in xrange(len(COPY_INPUT_DIRS)):
+        safe_delete_directory_tree(COPY_OUTPUT_DIRS[i])
+        copy_tasks += generate_copy_tasks_to_copy_from_source_to_target(
+            COPY_INPUT_DIRS[i], COPY_OUTPUT_DIRS[i], hashes)
+    _execute_tasks(copy_tasks)
+
+    SOURCE_DIRS = [
+        ASSETS_DEV_DIR, EXTENSIONS_DIRNAMES_TO_DIRPATHS['dev_dir'],
+        TEMPLATES_CORE_DIRNAMES_TO_DIRPATHS['dev_dir'],
+        THIRD_PARTY_GENERATED_DEV_DIR]
+    _verify_build(SOURCE_DIRS, COPY_OUTPUT_DIRS, hashes)
+    # Clean up un-hashed hashes.js.
+    safe_delete_file(HASHES_JS_FILEPATH)
     print 'Build completed.'
 
 
@@ -780,11 +1103,11 @@ def build():
     parser.add_option(
         '--prod_env', action='store_true', default=False, dest='prod_mode')
     options = parser.parse_args()[0]
-
+    # Regenerate /third_party/generated from scratch.
+    safe_delete_directory_tree(THIRD_PARTY_GENERATED_DEV_DIR)
+    build_third_party_libs(THIRD_PARTY_GENERATED_DEV_DIR)
     if options.prod_mode:
         generate_build_directory()
-    else:
-        build_third_party_libs()
 
 
 if __name__ == '__main__':
diff --git a/scripts/pre_commit_linter.py b/scripts/pre_commit_linter.py
--- a/scripts/pre_commit_linter.py
+++ b/scripts/pre_commit_linter.py
@@ -87,7 +87,7 @@
         'message': 'Please use spaces instead of tabs.',
         'excluded_files': (),
         'excluded_dirs': (
-            'assets/i18n/',)},
+            'assets/i18n/', 'core/tests/build_sources/assets/')},
     '\r': {
         'message': 'Please make sure all files only have LF endings (no CRLF).',
         'excluded_files': (),


Test patch:
diff --git a/core/tests/build_sources/README.md b/core/tests/build_sources/README.md
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/README.md
@@ -0,0 +1,14 @@
+TESTING INSTRUCTIONS
+
+This folder contains a few folder with source files representative of Oppia's
+repository to provide a deterministic test environment, that never changes,
+for build testing.
+
+- Changes, such as file deletion, file renaming, ...etc, to contents inside
+  core/tests/build_sources/assets, core/tests/build_sources/extensions, and
+  core/tests/build_sources/templates will affect test cases in build_test.py.
+
+- Processed, minified or built files should go to core/tests/build.
+
+- core/tests/build should be clean out at the end of each test run to prevent
+  adding new files to the directory.
diff --git a/core/tests/build_sources/assets/ab.json b/core/tests/build_sources/assets/ab.json
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/assets/ab.json
@@ -0,0 +1,154 @@
+{
+	"I18N_ABOUT_PAGE_ABOUT_TAB_HEADING": "Oppia иазкны",
+	"I18N_ABOUT_PAGE_CREDITS_TAB_HEADING": "Аҧҵаҩцәа",
+	"I18N_ABOUT_PAGE_FOUNDATION_TAB_DONATE_BUTTON": "Ацхыраара ҟаҵатәуп",
+	"I18N_ABOUT_PAGE_FOUNDATION_TAB_PARAGRAPH_4_HEADING": "Адиректор",
+	"I18N_ABOUT_PAGE_FOUNDATION_TAB_PARAGRAPH_5_LICENSE_HEADING": "Алицензиа",
+	"I18N_ABOUT_PAGE_TABS_CREDITS": "Аҧҵаҩцәа",
+	"I18N_ABOUT_PAGE_TITLE": "Ҳара иҳазкны - Oppia",
+	"I18N_ACTION_BROWSE_LESSONS": "Ирыхәаҧштәуп ҳара ҳаурокқәа",
+	"I18N_ACTION_CREATE_LESSON": "Иаҧышәҵа аурок",
+	"I18N_CONTACT_PAGE_PARAGRAPH_15_HEADING": "Ашәарҭадара",
+	"I18N_CREATE_COLLECTION": "Иаҧҵатәуп аколлекциа",
+	"I18N_CREATE_EXPLORATION_CREATE": "Иаҧҵатәуп",
+	"I18N_CREATE_EXPLORATION_UPLOAD": "Иҭагалатәуп",
+	"I18N_CREATE_NO_THANKS": "Мап, ҭабуп",
+	"I18N_CREATE_YES_PLEASE": "Ааи, бзиоуп!",
+	"I18N_DASHBOARD_COLLECTIONS": "Аколлекциақәа",
+	"I18N_DASHBOARD_EXPLORATIONS": "аҭҵаарақәа",
+	"I18N_DASHBOARD_EXPLORATIONS_SORT_BY_CATEGORY": "Акатегориа",
+	"I18N_DASHBOARD_EXPLORATIONS_SORT_BY_LAST_UPDATED": "Аҵыхәтәантәи арҿыцра",
+	"I18N_DASHBOARD_TABLE_HEADING_LAST_UPDATED": "Аҵыхәтәантәи арҿыцра",
+	"I18N_DASHBOARD_TABLE_HEADING_RATING": "Ареитинг",
+	"I18N_DIRECTIVES_UPLOAD_A_FILE": "Иҭагалатәуп афаил",
+	"I18N_ERROR_HEADER_400": "Агха 400",
+	"I18N_ERROR_HEADER_401": "Агха 401",
+	"I18N_ERROR_HEADER_404": "Агха 404",
+	"I18N_ERROR_HEADER_500": "Агха 500",
+	"I18N_ERROR_PAGE_TITLE_400": "Агха 400 - Oppia",
+	"I18N_ERROR_PAGE_TITLE_401": "Агха 401 - Oppia",
+	"I18N_ERROR_PAGE_TITLE_404": "Агха 404 - Oppia",
+	"I18N_ERROR_PAGE_TITLE_500": "Агха 500 - Oppia",
+	"I18N_FOOTER_BROWSE_LIBRARY": "Иахәаҧшлатәуп абиблиотека",
+	"I18N_FOOTER_CREDITS": "Аҧҵаҩцәа",
+	"I18N_FOOTER_DONATE": "Ацхыраара ҟаҵатәуп",
+	"I18N_FOOTER_FORUM": "Афорум",
+	"I18N_FOOTER_GET_STARTED": "Иалагатәуп",
+	"I18N_FOOTER_OPPIA_FOUNDATION": "Oppia Афонд",
+	"I18N_FOOTER_PRIVACY_POLICY": "Аконфиденциалтә политика",
+	"I18N_FOOTER_TEACH_LEARN_ALL_CAPS": "АРҴАРА/АҴАРА",
+	"I18N_FOOTER_TERMS_OF_SERVICE": "Ахархәара аҧҟарақәа",
+	"I18N_FORMS_TYPE_NUMBER": "Иҭажәгал ахыҧхьаӡара",
+	"I18N_GET_STARTED_PAGE_HEADING": "Иалагатәуп!",
+	"I18N_GET_STARTED_PAGE_TITLE": "Иалагатәуп!",
+	"I18N_INTERACTIONS_GRAPH_DELETE": "Ианыхтәуп",
+	"I18N_INTERACTIONS_GRAPH_ERROR_INVALID": "Ииашам аграфик!",
+	"I18N_INTERACTIONS_GRAPH_MOVE": "Ииагатәуп",
+	"I18N_INTERACTIONS_GRAPH_RESET_BUTTON": "Иқәгатәуп",
+	"I18N_INTERACTIONS_MUSIC_CLEAR": "Ирыцқьатәуп",
+	"I18N_INTERACTIONS_PENCILCODEEDITOR_CANCEL": "Аҟәыхра",
+	"I18N_INTERACTIONS_SET_INPUT_ADD_ITEM": "Иацҵатәуп аелемент",
+	"I18N_INTERACTIONS_SUBMIT": "Идәықәҵатәуп",
+	"I18N_LEARNER_DASHBOARD_COMPLETED_SECTION": "Ихыркәшоуп",
+	"I18N_LEARNER_DASHBOARD_FEEDBACK_THREAD_DEFAULT_MESSAGE": "Аҭак ҟаҵатәуп",
+	"I18N_LEARNER_DASHBOARD_REMOVE_BUTTON": "Ианыхтәуп",
+	"I18N_LEARNER_DASHBOARD_SEND_FEEDBACK_THREAD_MESSAGE": "Идәықәҵатәуп",
+	"I18N_LEARNER_DASHBOARD_SEND_FEEDBACK_THREAD_MESSAGE_IN_PROGRESS": "Адәықәҵара...",
+	"I18N_LIBRARY_ALL_CATEGORIES": "Акатегориақәа зегьы",
+	"I18N_LIBRARY_ALL_LANGUAGES": "Абызшәақәа зегьы",
+	"I18N_LIBRARY_CATEGORIES_ALGORITHMS": "Алгоритмқәа",
+	"I18N_LIBRARY_CATEGORIES_ARCHITECTURE": "Архитектура",
+	"I18N_LIBRARY_CATEGORIES_ART": "Аҟазара",
+	"I18N_LIBRARY_CATEGORIES_BIOLOGY": "Абиологиа",
+	"I18N_LIBRARY_CATEGORIES_BUSINESS": "Абизнес",
+	"I18N_LIBRARY_CATEGORIES_CHEMISTRY": "Ахимиа",
+	"I18N_LIBRARY_CATEGORIES_CODING": "Акодркра",
+	"I18N_LIBRARY_CATEGORIES_ECONOMICS": "Аекономика",
+	"I18N_LIBRARY_CATEGORIES_EDUCATION": "Аҵара",
+	"I18N_LIBRARY_CATEGORIES_ENGLISH": "Англыз бызшәа",
+	"I18N_LIBRARY_CATEGORIES_GEOGRAPHY": "Агеографиа",
+	"I18N_LIBRARY_CATEGORIES_HISTORY": "Аҭоурых",
+	"I18N_LIBRARY_CATEGORIES_HOBBIES": "Хобби",
+	"I18N_LIBRARY_CATEGORIES_LANGUAGES": "Абызшәақәа",
+	"I18N_LIBRARY_CATEGORIES_LAW": "Азакәан",
+	"I18N_LIBRARY_CATEGORIES_MATHEMATICS": "Аматематика",
+	"I18N_LIBRARY_CATEGORIES_MATHS": "Аматематика",
+	"I18N_LIBRARY_CATEGORIES_MEDICINE": "Амедицина",
+	"I18N_LIBRARY_CATEGORIES_MUSIC": "Амузыка",
+	"I18N_LIBRARY_CATEGORIES_PHILOSOPHY": "Афилософиа",
+	"I18N_LIBRARY_CATEGORIES_PROGRAMMING": "Апрограммаҧҵара",
+	"I18N_LIBRARY_CATEGORIES_PSYCHOLOGY": "Апсихологиа",
+	"I18N_LIBRARY_CATEGORIES_READING": "Аҧхьара",
+	"I18N_LIBRARY_CATEGORIES_RELIGION": "Арелигиа",
+	"I18N_LIBRARY_CATEGORIES_SPORT": "Аспорт",
+	"I18N_LIBRARY_CATEGORIES_STATISTICS": "Астатистика",
+	"I18N_LIBRARY_CATEGORIES_TEST": "Агәаҭара",
+	"I18N_LIBRARY_CATEGORIES_WELCOME": "Бзиала шәаабеит",
+	"I18N_LIBRARY_GROUPS_LANGUAGES": "Абызшәақәа",
+	"I18N_LIBRARY_GROUPS_MATHEMATICS_&_STATISTICS": "Аматематикеи астатистикеи",
+	"I18N_LIBRARY_GROUPS_SCIENCE": "Анаука",
+	"I18N_LIBRARY_GROUPS_SOCIAL_SCIENCE": "Аоциалтә наукақәа",
+	"I18N_LIBRARY_LAST_UPDATED": "Аҵыхәтәантәи арҿыцра",
+	"I18N_LIBRARY_LOADING": "Аҭагалара",
+	"I18N_LIBRARY_RATINGS_TOOLTIP": "Ареитингқәа",
+	"I18N_LIBRARY_VIEWS_TOOLTIP": "Ахәаҧшрақәа",
+	"I18N_LIBRARY_VIEW_ALL": "Ирыхәаҧштәуп зегьы",
+	"I18N_MODAL_CANCEL_BUTTON": "Аҟәыхра",
+	"I18N_PLAYER_AUDIO_EXPAND_TEXT": "Абжьы",
+	"I18N_PLAYER_AUDIO_LANGUAGE": "Абызшәа",
+	"I18N_PLAYER_AUDIO_LOADING_AUDIO": "Аудио аҭагалара...",
+	"I18N_PLAYER_BACK": "Шьҭахьҟа",
+	"I18N_PLAYER_BACK_TO_COLLECTION": "Шьҭахьҟа аколлекциақәа рахь",
+	"I18N_PLAYER_CONTRIBUTORS_TOOLTIP": "Авторцәа",
+	"I18N_PLAYER_EDIT_TOOLTIP": "Ириашатәуп",
+	"I18N_PLAYER_FORWARD": "Ҧхьаҟа",
+	"I18N_PLAYER_HINT": "Аҵаҳәара",
+	"I18N_PLAYER_HINT_NEED_A_HINT": "Иаҭахума аҵаҳәара?",
+	"I18N_PLAYER_HINT_REQUEST_STRING_1": "Сара исҭахын аҵаҳәара.",
+	"I18N_PLAYER_INFO_TOOLTIP": "Аинформациа",
+	"I18N_PLAYER_LAST_UPDATED_TOOLTIP": "Аҵыхәтәантәи арҿыцра",
+	"I18N_PLAYER_LOADING": "Аҭагалара...",
+	"I18N_PLAYER_NEXT_LESSON": "Анаҩстәи аурок",
+	"I18N_PLAYER_RATINGS_TOOLTIP": "Ареитингқәа",
+	"I18N_PLAYER_REPORT_MODAL_BODY_HEADER": "Проблемас иҟоузеи?",
+	"I18N_PLAYER_REPORT_MODAL_BODY_OTHER": "Даҽакы",
+	"I18N_PLAYER_REPORT_MODAL_FOOTER_CANCEL": "Аҟәыхра",
+	"I18N_PLAYER_REPORT_MODAL_FOOTER_SUBMIT": "Идәықәҵатәуп",
+	"I18N_PLAYER_REPORT_SUCCESS_MODAL_CLOSE": "Иарктәуп",
+	"I18N_PLAYER_REPORT_SUCCESS_MODAL_HEADER": "Иҭабуп!",
+	"I18N_PLAYER_RETURN_TO_LIBRARY": "Ихынҳәтәуп абиблиотекахь",
+	"I18N_PLAYER_SUBMIT_BUTTON": "Идәықәҵатәуп",
+	"I18N_PLAYER_TAGS_TOOLTIP": "Атегқәа",
+	"I18N_PLAYER_VIEWS_TOOLTIP": "Ахәаҧшрақәа",
+	"I18N_PREFERENCES_BIO": "Абиографиа",
+	"I18N_PREFERENCES_BREADCRUMB": "Архиарақәа",
+	"I18N_PREFERENCES_CANCEL_BUTTON": "Аҟәыхра",
+	"I18N_PREFERENCES_EMAIL": "Email",
+	"I18N_PREFERENCES_HEADING": "Архиарақәа",
+	"I18N_PREFERENCES_PICTURE": "Асахьа",
+	"I18N_PREFERENCES_SELECT_EXPLORATION_LANGUAGE": "Иалышәх шәыззыманшәалоу абызшәақәа...",
+	"I18N_PREFERENCES_USERNAME_NOT_SELECTED": "Макьана иалхӡам",
+	"I18N_SIDEBAR_BLOG": "Аблог",
+	"I18N_SIDEBAR_CONTACT_US": "Аконтактқәа",
+	"I18N_SIDEBAR_FORUM": "Афорум",
+	"I18N_SIDEBAR_GET_STARTED": "Иалагатәуп",
+	"I18N_SIDEBAR_LIBRARY_LINK": "Абиблиотека",
+	"I18N_SIGNUP_CC_TITLE": "Creative Commons Алицензиа",
+	"I18N_SIGNUP_CLOSE_BUTTON": "Иарктәуп",
+	"I18N_SIGNUP_EMAIL": "Email",
+	"I18N_SIGNUP_EMAIL_PREFERENCES": "Email архиарақәа",
+	"I18N_SIGNUP_LOADING": "Аҭагалара",
+	"I18N_SIGNUP_REGISTRATION": "Арегистрациа",
+	"I18N_SPLASH_JAVASCRIPT_ERROR_THANKS": "Иҭабуп.",
+	"I18N_TOPNAV_ABOUT_OPPIA": "Oppia иазкны",
+	"I18N_TOPNAV_ADMIN_PAGE": "Админ идаҟьа",
+	"I18N_TOPNAV_BLOG": "Аблог",
+	"I18N_TOPNAV_DONATE": "Ацхыраара ҟаҵатәуп",
+	"I18N_TOPNAV_FORUM": "Афорум",
+	"I18N_TOPNAV_GET_STARTED": "Иалагатәуп",
+	"I18N_TOPNAV_LIBRARY": "Абиблиотека",
+	"I18N_TOPNAV_LOGOUT": "Иҭыҵтәуп",
+	"I18N_TOPNAV_OPPIA_FOUNDATION": "Oppia Афонд",
+	"I18N_TOPNAV_PREFERENCES": "Архиарақәа",
+	"I18N_TOPNAV_SIGN_IN": "Иҭалатәуп"
+}
diff --git a/core/tests/build_sources/assets/collection.svg b/core/tests/build_sources/assets/collection.svg
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/assets/collection.svg
@@ -0,0 +1 @@
+<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 265.8 311"><defs><style>.cls-1{fill:#afd2eb;}.cls-2{opacity:0.22;}.cls-2,.cls-6{isolation:isolate;}.cls-3{fill:#fff;}.cls-4{fill:#d0982a;}.cls-5{fill:none;}</style></defs><title>collection</title><rect class="cls-1" width="265.8" height="311"/><rect class="cls-2" x="20.2" y="54.1" width="200" height="239.1"/><rect class="cls-3" x="22.1" y="50.9" width="200" height="240"/><rect class="cls-4" x="22" y="50.9" width="200" height="144"/><rect class="cls-5" x="86.2" y="80.8" width="74.4" height="74.4" transform="translate(-0.92 0.97) rotate(-0.45)"/><path class="cls-3" d="M123.2,86.4a31.7,31.7,0,1,0,31.9,31.5A31.77,31.77,0,0,0,123.2,86.4ZM123,94.3h0.8a2.17,2.17,0,0,1,1,1.9v3.6c0,1.1-.5,1.8-1.4,1.8s-1.3-.6-1.3-1.8,0.1-2.5,0-3.7A1.75,1.75,0,0,1,123,94.3Zm-20.2,12.5c0.9-1,1.3-1.1,2.5-.5a17.5,17.5,0,0,1,3.4,1.5,1.55,1.55,0,0,1,1.1,1.8,1.48,1.48,0,0,1-2.1.7c-1.4-.6-2.8-1.3-4.1-1.9a6,6,0,0,1-.8-0.8v-0.8Zm6.4,13.1c-1.5.3-3,.4-4.4,0.7a1.83,1.83,0,0,1-1.8-.9v-0.6a2,2,0,0,1,1.9-1.1,28.68,28.68,0,0,1,3.1-.5c0.3,0,.6-0.1,1-0.1a1.39,1.39,0,0,1,1.6,1.1C110.7,119.2,109.9,119.7,109.2,119.9ZM111,97.4a1.76,1.76,0,0,1,1.6.1c1.1,1.4,2,3,3,4.4a2.18,2.18,0,0,1,.1.6,2.28,2.28,0,0,1-.8.9,1.21,1.21,0,0,1-1.3-.1c-1.1-1.4-2-2.8-3-4.3A1,1,0,0,1,111,97.4ZM127.3,140h-8.2a1,1,0,0,1-1.2-1c-0.1-2.1-.1-2.1,2-2.1,2.7,0,5.3-.1,7.9,0,1.2,0,1.3,0,1.4,1.2C129.3,140,129.3,140,127.3,140Zm0-4.3h-8.2a1,1,0,0,1-1.2-1c-0.1-2.1-.1-2.1,2-2.1,2.7,0,5.3-.1,7.9,0,1.2,0,1.3,0,1.4,1.2C129.3,135.6,129.3,135.6,127.3,135.7Zm7.3-21.8a15.27,15.27,0,0,1-1.6,6,36.08,36.08,0,0,0-3.7,9.4c-0.2,1.3-.4,1.4-1.8,1.4h-8.2a1.13,1.13,0,0,1-1.3-.9c-0.2-.8-0.5-1.5-0.6-2.2a32.18,32.18,0,0,0-3.1-7.4,13.9,13.9,0,0,1-1.8-8,8.57,8.57,0,0,1,6.1-7.8,15,15,0,0,1,9.6-.1A9.33,9.33,0,0,1,134.6,113.9ZM136,99.1a28.16,28.16,0,0,1-2.4,3.6,1.34,1.34,0,0,1-2.1.4c-0.7-.4-0.8-1.2-0.3-2.1a26.37,26.37,0,0,1,2.3-3.5,2.06,2.06,0,0,1,1.7-.7c0.4,0,.7.7,1,1A1.86,1.86,0,0,1,136,99.1Zm8.1,20.2a1.68,1.68,0,0,1-2.1,1l-4.1-.6a1.46,1.46,0,0,1-1.3-1.6,1.29,1.29,0,0,1,1.5-1.2c1.6,0.2,3.2.5,4.6,0.8a4.7,4.7,0,0,1,1.2.9A1.08,1.08,0,0,1,144.1,119.3Zm-0.1-12a7.41,7.41,0,0,1-1.3,1.2c-1,.6-2.1,1-3.2,1.6a1.63,1.63,0,0,1-2.4-.6c-0.3-.7.1-1.5,1.1-2s2.3-.9,3.5-1.5,1.6-.4,2.4.7A1.27,1.27,0,0,0,144,107.3Z"/><g class="cls-6"><path class="cls-3" d="M38.27,172a0.7,0.7,0,0,1,.74-0.85h0.28a0.78,0.78,0,0,1,.39.07,1.25,1.25,0,0,1,.38.39l4.4,6.48a4.86,4.86,0,0,1,.4.68l0.28,0.59h0a0.36,0.36,0,0,0,0-.18q0-.18-0.07-0.52A4.18,4.18,0,0,1,45,178V171.6a0.43,0.43,0,0,1,.11-0.3,0.33,0.33,0,0,1,.26-0.12h0.9a0.32,0.32,0,0,1,.25.12,0.43,0.43,0,0,1,.11.3V181a0.54,0.54,0,0,1-.17.42,0.82,0.82,0,0,1-.52.26H45.56a0.89,0.89,0,0,1-.5-0.11,3.12,3.12,0,0,1-.57-0.67q-3.9-5.71-4.18-6.17l-0.57-1,0,0s0,0,0,.1,0,0.18.06,0.34,0,0.34.07,0.54q0.07,0.68.07,1.27v5.24a0.43,0.43,0,0,1-.11.29,0.33,0.33,0,0,1-.26.12h-0.9a0.32,0.32,0,0,1-.25-0.12,0.43,0.43,0,0,1-.11-0.29V172Z"/><path class="cls-3" d="M50.66,178.18v0.13a2,2,0,0,0,.59,1.5,2.32,2.32,0,0,0,1.69.6,3.09,3.09,0,0,0,1.8-.58,0.51,0.51,0,0,1,.28-0.11,0.41,0.41,0,0,1,.38.25l0.18,0.38a0.71,0.71,0,0,1,.06.29,0.52,0.52,0,0,1-.14.33,1.45,1.45,0,0,1-.37.3,2.74,2.74,0,0,1-.52.24,5.46,5.46,0,0,1-.61.17,6.19,6.19,0,0,1-1.31.14,3.91,3.91,0,0,1-1.38-.26,3.36,3.36,0,0,1-1.2-.76A3.82,3.82,0,0,1,49,177.95q0-3.17,2.23-3.94a4.39,4.39,0,0,1,1.43-.24,4.35,4.35,0,0,1,1.25.16,2.57,2.57,0,0,1,.89.46,2.21,2.21,0,0,1,.79,1.81,2,2,0,0,1-.93,1.74,3.9,3.9,0,0,1-2.15.53A7.27,7.27,0,0,1,50.66,178.18Zm3.36-2q0-1-1.31-1a1.82,1.82,0,0,0-1.75.92,2.14,2.14,0,0,0-.23.79,3.34,3.34,0,0,0,1.5.29Q54,177.21,54,176.19Z"/><path class="cls-3" d="M68,174a0.38,0.38,0,0,1,.44.41,14.59,14.59,0,0,1-.67,3.89,8.37,8.37,0,0,1-1.32,2.8,1.51,1.51,0,0,1-1.18.7,1.42,1.42,0,0,1-1-.31,1.68,1.68,0,0,1-.48-0.9q-0.37-1.54-.52-2.1l-0.26-1-0.17-.59,0,0.18-0.06.27-0.14.59A16.89,16.89,0,0,1,62,179.9q-0.22.5-.38,0.78a3,3,0,0,1-.37.52,1.43,1.43,0,0,1-1.08.55,1.47,1.47,0,0,1-1.53-1.31q-0.35-1.47-.79-2.91L57,174.64a0.83,0.83,0,0,1,0-.27,0.39,0.39,0,0,1,.46-0.42h0.81a0.59,0.59,0,0,1,.56.51q0.4,1.4.7,2.75L60,179.91a0.26,0.26,0,0,0,.19.25,0.38,0.38,0,0,0,.28-0.27q0.13-.27.29-0.65c0.1-.26.2-0.53,0.3-0.83s0.19-.6.28-0.92,0.18-.63.25-1q0.23-1,.38-1.78l0-.29a0.61,0.61,0,0,1,.72-0.41,1.8,1.8,0,0,1,.55.05,0.94,0.94,0,0,1,.43.57q0.17,0.46.37,1.12t0.38,1.37Q65,179,65.17,180a0.2,0.2,0,0,0,.14.2,0.58,0.58,0,0,0,.33-0.36,6.07,6.07,0,0,0,.41-1,13,13,0,0,0,.37-1.37,16.3,16.3,0,0,0,.36-3,0.39,0.39,0,0,1,.3-0.43l0.14,0H68Z"/><path class="cls-3" d="M81.7,173.4a0.71,0.71,0,0,1-.75.85H80.48a0.4,0.4,0,0,1-.34-0.16,0.82,0.82,0,0,1-.14-0.45,1,1,0,0,0-.17-0.52,1.22,1.22,0,0,0-.4-0.38,2.41,2.41,0,0,0-1.22-.27,2.64,2.64,0,0,0-1.32.32,2.56,2.56,0,0,0-.9.85,4.79,4.79,0,0,0-.66,2.67q0,3,2,3.77a3.24,3.24,0,0,0,1.18.23,4.23,4.23,0,0,0,1.83-.38l0.38-.18a0.82,0.82,0,0,1,.33-0.08,0.37,0.37,0,0,1,.23.07,1.06,1.06,0,0,1,.22.33,1,1,0,0,1,.13.44,0.63,0.63,0,0,1-.13.37,1.36,1.36,0,0,1-.32.32,3.15,3.15,0,0,1-.81.36,7,7,0,0,1-2,.26,4.86,4.86,0,0,1-2.06-.42,4.28,4.28,0,0,1-1.52-1.16,5.92,5.92,0,0,1-1.26-3.94,5.56,5.56,0,0,1,1.28-3.84,4.55,4.55,0,0,1,3.6-1.48,4.37,4.37,0,0,1,2.23.56A2,2,0,0,1,81.7,173.4Z"/><path class="cls-3" d="M82.85,177.78a4.35,4.35,0,0,1,.3-1.63,3.79,3.79,0,0,1,.82-1.27,4.13,4.13,0,0,1,5.61,0,4.11,4.11,0,0,1,1,2.91,4,4,0,0,1-1.11,2.93,4,4,0,0,1-5.54,0A4.07,4.07,0,0,1,82.85,177.78ZM85,176a2.64,2.64,0,0,0-.37.86,4.16,4.16,0,0,0-.12,1,3.35,3.35,0,0,0,.13.95,2.52,2.52,0,0,0,.4.81,1.94,1.94,0,0,0,1.64.78,1.88,1.88,0,0,0,1.63-.77,3,3,0,0,0,.49-1.83,2.52,2.52,0,0,0-1.13-2.39,2.26,2.26,0,0,0-2,0A1.94,1.94,0,0,0,85,176Z"/><path class="cls-3" d="M94.29,179.78q0,0.48.34,0.49a0.87,0.87,0,0,0,.28-0.06,1.81,1.81,0,0,1,.38-0.12,0.28,0.28,0,0,1,.25.2l0.21,0.49a0.52,0.52,0,0,1,0,.19,0.6,0.6,0,0,1-.26.38,2.06,2.06,0,0,1-1.36.43q-1.53,0-1.53-2V170a0.47,0.47,0,0,1,.11-0.31,0.33,0.33,0,0,1,.26-0.13h0.92a0.32,0.32,0,0,1,.26.13,0.49,0.49,0,0,1,.1.31v9.8Z"/><path class="cls-3" d="M99,179.78q0,0.48.34,0.49a0.87,0.87,0,0,0,.28-0.06,1.81,1.81,0,0,1,.38-0.12,0.28,0.28,0,0,1,.25.2l0.21,0.49a0.52,0.52,0,0,1,0,.19,0.6,0.6,0,0,1-.26.38,2.06,2.06,0,0,1-1.36.43q-1.53,0-1.53-2V170a0.47,0.47,0,0,1,.11-0.31,0.33,0.33,0,0,1,.26-0.13H98.6a0.32,0.32,0,0,1,.26.13,0.49,0.49,0,0,1,.1.31v9.8Z"/><path class="cls-3" d="M103.11,178.18v0.13a2,2,0,0,0,.59,1.5,2.32,2.32,0,0,0,1.69.6,3.09,3.09,0,0,0,1.8-.58,0.51,0.51,0,0,1,.28-0.11,0.41,0.41,0,0,1,.38.25l0.18,0.38a0.71,0.71,0,0,1,.06.29,0.52,0.52,0,0,1-.14.33,1.45,1.45,0,0,1-.37.3,2.74,2.74,0,0,1-.52.24,5.46,5.46,0,0,1-.61.17,6.19,6.19,0,0,1-1.31.14,3.91,3.91,0,0,1-1.38-.26,3.36,3.36,0,0,1-1.2-.76,3.82,3.82,0,0,1-1.09-2.86q0-3.17,2.23-3.94a4.39,4.39,0,0,1,1.43-.24,4.35,4.35,0,0,1,1.25.16,2.57,2.57,0,0,1,.89.46,2.21,2.21,0,0,1,.79,1.81,2,2,0,0,1-.93,1.74,3.9,3.9,0,0,1-2.15.53A7.27,7.27,0,0,1,103.11,178.18Zm3.36-2q0-1-1.31-1a1.82,1.82,0,0,0-1.75.92,2.14,2.14,0,0,0-.23.79,3.34,3.34,0,0,0,1.5.29Q106.47,177.21,106.47,176.19Z"/><path class="cls-3" d="M115.27,177a0.49,0.49,0,0,1-.56-0.55,1.21,1.21,0,0,0-.39-1,1.31,1.31,0,0,0-.9-0.29,1.72,1.72,0,0,0-.93.23,1.93,1.93,0,0,0-.6.59,3.12,3.12,0,0,0-.45,1.71,2.48,2.48,0,0,0,1.13,2.45,2,2,0,0,0,.91.21,2.58,2.58,0,0,0,.82-0.11,4.26,4.26,0,0,0,.57-0.24l0.44-.24a0.66,0.66,0,0,1,.31-0.11,0.44,0.44,0,0,1,.39.23l0.15,0.25a0.69,0.69,0,0,1,.1.34,0.59,0.59,0,0,1-.14.35,1.46,1.46,0,0,1-.37.33,3.2,3.2,0,0,1-.52.27,5.39,5.39,0,0,1-.61.21,4.14,4.14,0,0,1-2.75-.12,3.36,3.36,0,0,1-1.92-2.11,5.12,5.12,0,0,1-.26-1.68,4.72,4.72,0,0,1,.28-1.67,3.61,3.61,0,0,1,.77-1.25,3.53,3.53,0,0,1,2.66-1.05,3,3,0,0,1,2.22.76,2.21,2.21,0,0,1,.65,1.63,0.74,0.74,0,0,1-.85.83h-0.16Z"/><path class="cls-3" d="M122.46,175a0.36,0.36,0,0,1-.43.38H120.1v4.29a0.61,0.61,0,0,0,.69.7,2.36,2.36,0,0,0,1.14-.44,0.53,0.53,0,0,1,.3-0.09,0.3,0.3,0,0,1,.26.23l0.16,0.36a0.91,0.91,0,0,1,.09.35,0.5,0.5,0,0,1-.22.33,2.2,2.2,0,0,1-.55.34,3.4,3.4,0,0,1-1.37.34,3,3,0,0,1-1-.13,1.61,1.61,0,0,1-.61-0.38,2.05,2.05,0,0,1-.55-1.54v-4.36h-0.57a0.36,0.36,0,0,1-.43-0.39v-0.39a0.35,0.35,0,0,1,.08-0.25,0.61,0.61,0,0,1,.32-0.14l0.68-.2,0.26-1.61a0.42,0.42,0,0,1,.37-0.45h0.57a0.32,0.32,0,0,1,.26.13,0.5,0.5,0,0,1,.1.32V174H122a0.36,0.36,0,0,1,.43.38V175Z"/><path class="cls-3" d="M124.94,172.63a1.09,1.09,0,0,1-.79-0.32,1.05,1.05,0,0,1-.33-0.79,1.1,1.1,0,0,1,1.12-1.11,1,1,0,0,1,1,.67,1.12,1.12,0,0,1-.24,1.23A1.09,1.09,0,0,1,124.94,172.63Zm0.8,7.15q0,0.48.32,0.49a0.61,0.61,0,0,0,.25-0.06,1.17,1.17,0,0,1,.36-0.12,0.27,0.27,0,0,1,.23.2l0.21,0.48a0.51,0.51,0,0,1,.05.22,0.72,0.72,0,0,1-.51.55,2.06,2.06,0,0,1-.95.25,2.23,2.23,0,0,1-.75-0.1,1.11,1.11,0,0,1-.48-0.34,2.56,2.56,0,0,1-.4-1.6V174.4a0.48,0.48,0,0,1,.11-0.32,0.33,0.33,0,0,1,.26-0.13h0.92a0.32,0.32,0,0,1,.26.13,0.5,0.5,0,0,1,.1.32v5.38Z"/><path class="cls-3" d="M128.34,177.78a4.35,4.35,0,0,1,.3-1.63,3.79,3.79,0,0,1,.82-1.27,4.13,4.13,0,0,1,5.61,0,4.11,4.11,0,0,1,1,2.91,4,4,0,0,1-1.11,2.93,4,4,0,0,1-5.54,0A4.07,4.07,0,0,1,128.34,177.78Zm2.2-1.74a2.64,2.64,0,0,0-.37.86,4.16,4.16,0,0,0-.12,1,3.35,3.35,0,0,0,.13.95,2.52,2.52,0,0,0,.4.81,1.94,1.94,0,0,0,1.64.78,1.88,1.88,0,0,0,1.63-.77,3,3,0,0,0,.49-1.83,2.52,2.52,0,0,0-1.13-2.39,2.26,2.26,0,0,0-2,0A1.94,1.94,0,0,0,130.53,176Z"/><path class="cls-3" d="M138.11,174.44a0.43,0.43,0,0,1,.47-0.49H139a0.52,0.52,0,0,1,.52.29,4.92,4.92,0,0,1,.33,1.08,2.82,2.82,0,0,1,1.83-1.44,3.76,3.76,0,0,1,1-.1,2.32,2.32,0,0,1,1.06.24,1.92,1.92,0,0,1,.77.67,4.09,4.09,0,0,1,.52,2.28v4.25a0.45,0.45,0,0,1-.12.32,0.47,0.47,0,0,1-.36.13h-0.67a0.5,0.5,0,0,1-.38-0.13,0.45,0.45,0,0,1-.12-0.32V177q0-1.75-1.29-1.75a1.9,1.9,0,0,0-1.37.63,2.18,2.18,0,0,0-.66,1.57v3.8a0.46,0.46,0,0,1-.12.32,0.45,0.45,0,0,1-.36.13h-0.7a0.48,0.48,0,0,1-.36-0.13,0.45,0.45,0,0,1-.12-0.32V177a13.79,13.79,0,0,0-.09-1.92C138.14,174.73,138.11,174.52,138.11,174.44Z"/></g><g class="cls-6"><path d="M39.52,215.58a2.5,2.5,0,0,0,1.9.71,2.65,2.65,0,0,0,1.82-.53,2.59,2.59,0,0,0,.78-1.37,0.37,0.37,0,0,1,.13-0.22,0.36,0.36,0,0,1,.23-0.08H45a0.31,0.31,0,0,1,.31.31,3.13,3.13,0,0,1-.45,1.42A3.39,3.39,0,0,1,43.59,217a4.31,4.31,0,0,1-2.18.5,4.17,4.17,0,0,1-2.06-.49A3.46,3.46,0,0,1,38,215.71a4.27,4.27,0,0,1-.53-2q0-.42,0-1.25t0-1.25a4.26,4.26,0,0,1,.53-2A3.46,3.46,0,0,1,39.35,208a4.17,4.17,0,0,1,2.06-.49,4.3,4.3,0,0,1,2.18.5,3.39,3.39,0,0,1,1.29,1.24,3.13,3.13,0,0,1,.45,1.42,0.31,0.31,0,0,1-.31.31H44.38a0.36,0.36,0,0,1-.23-0.08,0.37,0.37,0,0,1-.13-0.22,2.6,2.6,0,0,0-.78-1.37,2.65,2.65,0,0,0-1.82-.53,2.5,2.5,0,0,0-1.9.71,2.77,2.77,0,0,0-.72,1.9q0,0.42,0,1.18t0,1.18A2.77,2.77,0,0,0,39.52,215.58Z"/><path d="M51,210.24a0.37,0.37,0,0,1,.11.27V211a0.38,0.38,0,0,1-.38.38H49.9a1.45,1.45,0,0,0-1.61,1.61V217a0.38,0.38,0,0,1-.38.38H47.39A0.38,0.38,0,0,1,47,217v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a1.75,1.75,0,0,1,.67-0.62,2.29,2.29,0,0,1,1-.2h0.73A0.37,0.37,0,0,1,51,210.24Z"/><path d="M57.39,210.94a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H53.06v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41A3,3,0,0,1,52.31,216a4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.06,3.06,0,0,1,1.11-1.11A3.11,3.11,0,0,1,55,210,3,3,0,0,1,57.39,210.94ZM57,213.11v0a1.79,1.79,0,0,0-.54-1.37A2,2,0,0,0,55,211.2a2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0H57Z"/><path d="M64.27,212.8v-0.18a1.29,1.29,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.71,1.71,0,0,0-.87.19,2.11,2.11,0,0,0-.59.57,0.46,0.46,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0H60.3A0.38,0.38,0,0,1,60,212a0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.29,3.29,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.62,2.62,0,0,1,.8,2V217a0.38,0.38,0,0,1-.38.38H64.65a0.38,0.38,0,0,1-.38-0.38v-0.57a1.64,1.64,0,0,1-1,.8,3.86,3.86,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.62,4.62,0,0,1,2.09-.81Zm0,1.12-1.65.27a3.77,3.77,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M69.66,215.88a1.11,1.11,0,0,0,.93.32h0.74a0.38,0.38,0,0,1,.38.38V217a0.38,0.38,0,0,1-.38.38H70.45a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1v-2.28a0.38,0.38,0,0,1,.38-0.38H69a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38V211a0.38,0.38,0,0,1-.38.38H69.41V215A1.5,1.5,0,0,0,69.66,215.88Z"/><path d="M78.34,210.94a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H74v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41A3,3,0,0,1,73.27,216a4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.06,3.06,0,0,1,1.11-1.11A3.11,3.11,0,0,1,76,210,3,3,0,0,1,78.34,210.94ZM78,213.11v0a1.79,1.79,0,0,0-.54-1.37A2,2,0,0,0,76,211.2a2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0H78Z"/><path d="M88.65,212.8v-0.18a1.29,1.29,0,0,0-.46-1.06A1.81,1.81,0,0,0,87,211.2a1.71,1.71,0,0,0-.87.19,2.11,2.11,0,0,0-.59.57,0.46,0.46,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0H84.67A0.38,0.38,0,0,1,84.4,212a0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68A3.29,3.29,0,0,1,87,210a3,3,0,0,1,2.11.73,2.62,2.62,0,0,1,.8,2V217a0.38,0.38,0,0,1-.38.38H89a0.38,0.38,0,0,1-.38-0.38v-0.57a1.64,1.64,0,0,1-1,.8,3.86,3.86,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.62,4.62,0,0,1,2.09-.81Zm0,1.12-1.65.27a3.77,3.77,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M100.21,215.25a0.89,0.89,0,0,1,.2-0.29,0.4,0.4,0,0,1,.27-0.08h0.42a0.37,0.37,0,0,1,.27.11,0.32,0.32,0,0,1,.11.25,1.82,1.82,0,0,1-.38,1,3,3,0,0,1-1.09.92,3.54,3.54,0,0,1-1.68.38,3.38,3.38,0,0,1-1.72-.42A2.87,2.87,0,0,1,95.48,216a3.83,3.83,0,0,1-.43-1.65c0-.11,0-0.33,0-0.64s0-.38,0-0.48a3.77,3.77,0,0,1,1-2.32,3,3,0,0,1,2.31-.9,3.56,3.56,0,0,1,1.67.37,2.92,2.92,0,0,1,1.08.91,2,2,0,0,1,.4,1,0.31,0.31,0,0,1-.1.27,0.39,0.39,0,0,1-.27.11h-0.42a0.4,0.4,0,0,1-.27-0.08,0.88,0.88,0,0,1-.2-0.29,1.87,1.87,0,0,0-1.89-1.09,1.94,1.94,0,0,0-1.34.52,2.27,2.27,0,0,0-.66,1.57q0,0.15,0,.5c0,0.21,0,.37,0,0.48a2.33,2.33,0,0,0,.67,1.57,1.92,1.92,0,0,0,1.34.52A1.87,1.87,0,0,0,100.21,215.25Z"/><path d="M109.48,213.79q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C109.47,213.32,109.48,213.54,109.48,213.79Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,107.58,211.74Z"/><path d="M112.39,217.3a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38V217A0.36,0.36,0,0,1,112.39,217.3Z"/><path d="M115.76,217.3a0.36,0.36,0,0,1-.27.11H115a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38V217A0.36,0.36,0,0,1,115.76,217.3Z"/><path d="M123.21,210.94a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38h-4.82v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.06,3.06,0,0,1,1.11-1.11,3.11,3.11,0,0,1,1.6-.41A3,3,0,0,1,123.21,210.94Zm-0.39,2.17v0a1.79,1.79,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0h3.93Z"/><path d="M130.6,215.25a0.89,0.89,0,0,1,.2-0.29,0.4,0.4,0,0,1,.27-0.08h0.42a0.37,0.37,0,0,1,.27.11,0.32,0.32,0,0,1,.11.25,1.82,1.82,0,0,1-.38,1,3,3,0,0,1-1.09.92,3.54,3.54,0,0,1-1.68.38,3.38,3.38,0,0,1-1.72-.42,2.87,2.87,0,0,1-1.12-1.15,3.83,3.83,0,0,1-.43-1.65c0-.11,0-0.33,0-0.64s0-.38,0-0.48a3.77,3.77,0,0,1,1-2.32,3,3,0,0,1,2.31-.9,3.56,3.56,0,0,1,1.67.37,2.92,2.92,0,0,1,1.08.91,2,2,0,0,1,.4,1,0.31,0.31,0,0,1-.1.27,0.39,0.39,0,0,1-.27.11h-0.42a0.4,0.4,0,0,1-.27-0.08,0.88,0.88,0,0,1-.2-0.29,1.87,1.87,0,0,0-1.89-1.09,1.94,1.94,0,0,0-1.34.52,2.27,2.27,0,0,0-.66,1.57q0,0.15,0,.5c0,0.21,0,.37,0,0.48a2.33,2.33,0,0,0,.67,1.57,1.92,1.92,0,0,0,1.34.52A1.87,1.87,0,0,0,130.6,215.25Z"/><path d="M135.69,215.88a1.11,1.11,0,0,0,.93.32h0.74a0.38,0.38,0,0,1,.38.38V217a0.38,0.38,0,0,1-.38.38h-0.88a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1v-2.28a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38V211a0.38,0.38,0,0,1-.38.38h-1.78V215A1.5,1.5,0,0,0,135.69,215.88Z"/><path d="M140.45,208.91a0.36,0.36,0,0,1-.27.11h-0.8a0.38,0.38,0,0,1-.38-0.38v-0.8a0.38,0.38,0,0,1,.38-0.38h0.8a0.38,0.38,0,0,1,.38.38v0.8A0.36,0.36,0,0,1,140.45,208.91Zm-0.14,8.39a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38H140a0.38,0.38,0,0,1,.38.38V217A0.36,0.36,0,0,1,140.31,217.3Z"/><path d="M148.85,213.79q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C148.84,213.32,148.85,213.54,148.85,213.79Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.59,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.59,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.59-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,147,211.74Z"/><path d="M157,217.3a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-3.7a2.23,2.23,0,0,0-.52-1.6,2.17,2.17,0,0,0-2.88,0,2.22,2.22,0,0,0-.52,1.6V217a0.38,0.38,0,0,1-.38.38H151a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.41,2.41,0,0,1,.85-0.68A2.91,2.91,0,0,1,154,210a3.19,3.19,0,0,1,1.57.38,2.67,2.67,0,0,1,1.07,1.08,3.45,3.45,0,0,1,.38,1.67V217A0.36,0.36,0,0,1,157,217.3Z"/><path d="M168.84,213.79q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C168.83,213.32,168.84,213.54,168.84,213.79Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.59,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.59,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.59-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,166.94,211.74Z"/><path d="M174.3,210.13a0.38,0.38,0,0,1,.38.38V211a0.38,0.38,0,0,1-.38.38h-1.78V217a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38v-5.7h-1.18a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1.18v-0.7a2.52,2.52,0,0,1,.56-1.79,2.32,2.32,0,0,1,1.75-.59h0.88a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H173.7a1.11,1.11,0,0,0-.93.32,1.5,1.5,0,0,0-.25.92v0.63h1.78Z"/></g><g class="cls-6"><path d="M42.91,227.74a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H38.58v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.06,3.06,0,0,1,1.11-1.11,3.11,3.11,0,0,1,1.6-.41A3,3,0,0,1,42.91,227.74Zm-0.39,2.17v0a1.79,1.79,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0h3.93Z"/><path d="M46.1,233.95a1.49,1.49,0,0,1-.2.19,0.39,0.39,0,0,1-.25.08H45a0.32,0.32,0,0,1-.24-0.1,0.31,0.31,0,0,1-.1-0.22,0.34,0.34,0,0,1,.06-0.2l2.56-3.21-2.42-3a0.34,0.34,0,0,1-.06-0.2,0.31,0.31,0,0,1,.1-0.22,0.32,0.32,0,0,1,.24-0.1h0.67a0.4,0.4,0,0,1,.25.08,1.5,1.5,0,0,1,.2.19l1.86,2.34L50,227.2a1.47,1.47,0,0,1,.2-0.19,0.4,0.4,0,0,1,.25-0.08h0.64a0.32,0.32,0,0,1,.24.1,0.31,0.31,0,0,1,.1.22,0.35,0.35,0,0,1-.06.2l-2.44,3.07,2.53,3.18a0.34,0.34,0,0,1,.06.2,0.31,0.31,0,0,1-.1.22,0.32,0.32,0,0,1-.24.1H50.5a0.39,0.39,0,0,1-.25-0.08,1.43,1.43,0,0,1-.2-0.19l-2-2.48Z"/><path d="M54.26,236.49a0.38,0.38,0,0,1-.38.38H53.37a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.54,2.54,0,0,1,2.17-1,3.11,3.11,0,0,1,1.57.41,3,3,0,0,1,1.13,1.16,3.86,3.86,0,0,1,.47,1.76q0,0.14,0,.45c0,0.21,0,.37,0,0.46a3.81,3.81,0,0,1-.47,1.76A3.06,3.06,0,0,1,58,233.95a3.1,3.1,0,0,1-1.58.41,2.91,2.91,0,0,1-1.32-.27,2.4,2.4,0,0,1-.85-0.68v3.09Zm0.59-3.89a2,2,0,0,0,1.44.55,1.87,1.87,0,0,0,1.45-.59,2.49,2.49,0,0,0,.59-1.6c0-.09,0-0.22,0-0.39s0-.3,0-0.39a2.49,2.49,0,0,0-.59-1.6,1.87,1.87,0,0,0-1.45-.59,2,2,0,0,0-1.43.54,2,2,0,0,0-.6,1.43v1.11A2.2,2.2,0,0,0,54.85,232.6Z"/><path d="M62.52,234.1a0.36,0.36,0,0,1-.27.11H61.74a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v9.18A0.36,0.36,0,0,1,62.52,234.1Z"/><path d="M71.06,230.58q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C71.05,230.12,71.06,230.34,71.06,230.58Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,69.17,228.54Z"/><path d="M76.78,227a0.37,0.37,0,0,1,.11.27v0.45a0.38,0.38,0,0,1-.38.38H75.7a1.45,1.45,0,0,0-1.61,1.61v4.09a0.38,0.38,0,0,1-.38.38H73.19a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a1.75,1.75,0,0,1,.67-0.62,2.29,2.29,0,0,1,1-.2h0.73A0.37,0.37,0,0,1,76.78,227Z"/><path d="M82.32,229.6v-0.18a1.29,1.29,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.71,1.71,0,0,0-.87.19,2.11,2.11,0,0,0-.59.57,0.46,0.46,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0H78.34a0.38,0.38,0,0,1-.27-0.11,0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.29,3.29,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.62,2.62,0,0,1,.8,2v4.27a0.38,0.38,0,0,1-.38.38H82.7a0.38,0.38,0,0,1-.38-0.38v-0.57a1.64,1.64,0,0,1-1,.8,3.86,3.86,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.62,4.62,0,0,1,2.09-.81Zm0,1.12-1.65.27a3.77,3.77,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M87.7,232.68a1.11,1.11,0,0,0,.93.32h0.74a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H88.49a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1v-2.28a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H87.46v3.63A1.5,1.5,0,0,0,87.7,232.68Z"/><path d="M92.46,225.71a0.36,0.36,0,0,1-.27.11h-0.8a0.38,0.38,0,0,1-.38-0.38v-0.8a0.38,0.38,0,0,1,.38-0.38h0.8a0.38,0.38,0,0,1,.38.38v0.8A0.36,0.36,0,0,1,92.46,225.71Zm-0.14,8.39a0.36,0.36,0,0,1-.27.11H91.53a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38H92a0.38,0.38,0,0,1,.38.38v6.52A0.36,0.36,0,0,1,92.32,234.1Z"/><path d="M100.86,230.58q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C100.85,230.12,100.86,230.34,100.86,230.58Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,99,228.54Z"/><path d="M109,234.1a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-3.7a2.23,2.23,0,0,0-.52-1.6,2.17,2.17,0,0,0-2.88,0,2.21,2.21,0,0,0-.52,1.6v3.7a0.38,0.38,0,0,1-.38.38H103a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.41,2.41,0,0,1,.85-0.68,2.91,2.91,0,0,1,1.32-.27,3.2,3.2,0,0,1,1.57.38,2.67,2.67,0,0,1,1.07,1.08,3.45,3.45,0,0,1,.39,1.67v3.91A0.36,0.36,0,0,1,109,234.1Z"/><path d="M110.81,228.89a1.89,1.89,0,0,1,.31-1,2.14,2.14,0,0,1,.92-0.76,3.48,3.48,0,0,1,1.49-.29,4,4,0,0,1,1.5.25,2.46,2.46,0,0,1,1,.63,1.14,1.14,0,0,1,.33.73,0.32,0.32,0,0,1-.11.24,0.38,0.38,0,0,1-.27.11h-0.46a0.5,0.5,0,0,1-.42-0.2,1.82,1.82,0,0,0-1.53-.56,2,2,0,0,0-1,.24,0.73,0.73,0,0,0-.4.66,0.69,0.69,0,0,0,.2.53,1.44,1.44,0,0,0,.59.29q0.39,0.11,1.39.34a3.37,3.37,0,0,1,1.75.81,1.86,1.86,0,0,1,.52,1.33,1.77,1.77,0,0,1-.36,1.06,2.5,2.5,0,0,1-1,.79,3.71,3.71,0,0,1-1.55.3,4,4,0,0,1-1.53-.27,2.87,2.87,0,0,1-1-.67,1.13,1.13,0,0,1-.36-0.74,0.33,0.33,0,0,1,.11-0.25,0.37,0.37,0,0,1,.27-0.1h0.5a0.82,0.82,0,0,1,.22,0,0.35,0.35,0,0,1,.18.15,1.88,1.88,0,0,0,1.64.64,2.36,2.36,0,0,0,1.15-.27,0.79,0.79,0,0,0,.5-0.68,0.66,0.66,0,0,0-.24-0.54,2,2,0,0,0-.71-0.34q-0.46-.13-1.44-0.34Q110.81,230.54,110.81,228.89Z"/></g><rect class="cls-2" x="32.5" y="50.9" width="189.6" height="224.6"/><rect class="cls-3" x="34.5" y="32.5" width="200" height="240"/><rect class="cls-4" x="34.4" y="32.5" width="200" height="144"/><rect class="cls-5" x="98.6" y="62.4" width="74.4" height="74.4" transform="translate(-0.78 1.07) rotate(-0.45)"/><path class="cls-3" d="M135.6,67.9a31.7,31.7,0,1,0,31.9,31.5A31.77,31.77,0,0,0,135.6,67.9Zm-0.2,7.9h0.8a2.17,2.17,0,0,1,1,1.9v3.6c0,1.1-.5,1.8-1.4,1.8s-1.3-.6-1.3-1.8,0.1-2.5,0-3.7A1.75,1.75,0,0,1,135.4,75.8ZM115.2,88.4c0.9-1,1.3-1.1,2.5-.5a17.5,17.5,0,0,1,3.4,1.5,1.55,1.55,0,0,1,1.1,1.8,1.48,1.48,0,0,1-2.1.7c-1.4-.6-2.8-1.3-4.1-1.9a6,6,0,0,1-.8-0.8V88.4Zm6.4,13c-1.5.3-3,.4-4.4,0.7a1.83,1.83,0,0,1-1.8-.9v-0.6a2,2,0,0,1,1.9-1.1,28.68,28.68,0,0,1,3.1-.5c0.3,0,.6-0.1,1-0.1A1.39,1.39,0,0,1,123,100,1.43,1.43,0,0,1,121.6,101.4Zm1.8-22.5a1.76,1.76,0,0,1,1.6.1c1.1,1.4,2,3,3,4.4a2.18,2.18,0,0,1,.1.6,2.28,2.28,0,0,1-.8.9,1.21,1.21,0,0,1-1.3-.1c-1.1-1.4-2-2.8-3-4.3A1,1,0,0,1,123.4,78.9Zm16.3,42.7h-8.2a1,1,0,0,1-1.2-1c-0.1-2.1-.1-2.1,2-2.1,2.7,0,5.3-.1,7.9,0,1.2,0,1.3,0,1.4,1.2C141.7,121.5,141.7,121.5,139.7,121.6Zm0-4.4h-8.2a1,1,0,0,1-1.2-1c-0.1-2.1-.1-2.1,2-2.1,2.7,0,5.3-.1,7.9,0,1.2,0,1.3,0,1.4,1.2C141.7,117.2,141.7,117.2,139.7,117.2ZM147,95.5a15.27,15.27,0,0,1-1.6,6,36.08,36.08,0,0,0-3.7,9.4c-0.2,1.3-.4,1.4-1.8,1.4h-8.2a1.13,1.13,0,0,1-1.3-.9c-0.2-.8-0.5-1.5-0.6-2.2a32.18,32.18,0,0,0-3.1-7.4,13.9,13.9,0,0,1-1.8-8A8.57,8.57,0,0,1,131,86a15,15,0,0,1,9.6-.1A9.33,9.33,0,0,1,147,95.5Zm1.4-14.8a28.16,28.16,0,0,1-2.4,3.6,1.34,1.34,0,0,1-2.1.4c-0.7-.4-0.8-1.2-0.3-2.1a26.37,26.37,0,0,1,2.3-3.5,2.06,2.06,0,0,1,1.7-.7c0.4,0,.7.7,1,1A2.19,2.19,0,0,1,148.4,80.7Zm8.1,20.1a1.68,1.68,0,0,1-2.1,1l-4.1-.6a1.46,1.46,0,0,1-1.3-1.6,1.29,1.29,0,0,1,1.5-1.2c1.6,0.2,3.2.5,4.6,0.8a4.7,4.7,0,0,1,1.2.9A1.08,1.08,0,0,1,156.5,100.8Zm-0.1-12a7.41,7.41,0,0,1-1.3,1.2c-1,.6-2.1,1-3.2,1.6a1.63,1.63,0,0,1-2.4-.6c-0.3-.7.1-1.5,1.1-2s2.3-.9,3.5-1.5,1.6-.4,2.4.7A1.27,1.27,0,0,0,156.4,88.8Z"/><g class="cls-6"><path class="cls-3" d="M50.67,153.58a0.7,0.7,0,0,1,.74-0.85h0.28a0.78,0.78,0,0,1,.39.07,1.25,1.25,0,0,1,.38.39l4.4,6.48a4.86,4.86,0,0,1,.4.68l0.28,0.59h0a0.36,0.36,0,0,0,0-.18c0-.12,0-0.29-0.07-0.52a4.18,4.18,0,0,1-.05-0.67v-6.43a0.43,0.43,0,0,1,.11-0.3,0.33,0.33,0,0,1,.26-0.12h0.9a0.32,0.32,0,0,1,.25.12,0.43,0.43,0,0,1,.11.3v9.38a0.54,0.54,0,0,1-.17.42,0.82,0.82,0,0,1-.52.26H58a0.89,0.89,0,0,1-.5-0.11,3.12,3.12,0,0,1-.57-0.67q-3.9-5.71-4.18-6.17l-0.57-1,0,0s0,0,0,.1,0,0.18.06,0.34,0,0.34.07,0.54q0.07,0.68.07,1.27v5.24a0.43,0.43,0,0,1-.11.29,0.33,0.33,0,0,1-.26.12H51a0.32,0.32,0,0,1-.25-0.12,0.43,0.43,0,0,1-.11-0.29v-9.22Z"/><path class="cls-3" d="M63.06,159.74v0.13a2,2,0,0,0,.59,1.5,2.32,2.32,0,0,0,1.69.6,3.09,3.09,0,0,0,1.8-.58,0.51,0.51,0,0,1,.28-0.11,0.41,0.41,0,0,1,.38.25L68,161.92a0.71,0.71,0,0,1,.06.29,0.52,0.52,0,0,1-.14.33,1.45,1.45,0,0,1-.37.3,2.74,2.74,0,0,1-.52.24,5.46,5.46,0,0,1-.61.17,6.19,6.19,0,0,1-1.31.14,3.91,3.91,0,0,1-1.38-.26,3.36,3.36,0,0,1-1.2-.76,3.82,3.82,0,0,1-1.09-2.86q0-3.17,2.23-3.94a4.39,4.39,0,0,1,1.43-.24,4.35,4.35,0,0,1,1.25.16,2.57,2.57,0,0,1,.89.46,2.21,2.21,0,0,1,.79,1.81,2,2,0,0,1-.93,1.74A3.9,3.9,0,0,1,65,160,7.27,7.27,0,0,1,63.06,159.74Zm3.36-2q0-1-1.31-1a1.82,1.82,0,0,0-1.75.92,2.14,2.14,0,0,0-.23.79,3.34,3.34,0,0,0,1.5.29Q66.42,158.76,66.42,157.74Z"/><path class="cls-3" d="M80.42,155.5a0.38,0.38,0,0,1,.44.41,14.59,14.59,0,0,1-.67,3.89,8.37,8.37,0,0,1-1.32,2.8,1.51,1.51,0,0,1-1.18.7,1.42,1.42,0,0,1-1-.31,1.68,1.68,0,0,1-.48-0.9q-0.37-1.54-.52-2.1l-0.26-1-0.17-.59,0,0.18-0.06.27L75,159.5a16.89,16.89,0,0,1-.63,1.95q-0.22.5-.38,0.78a3,3,0,0,1-.37.52,1.43,1.43,0,0,1-1.08.55A1.47,1.47,0,0,1,71.06,162q-0.35-1.47-.79-2.91l-0.91-2.88a0.83,0.83,0,0,1,0-.27,0.39,0.39,0,0,1,.46-0.42h0.81a0.59,0.59,0,0,1,.56.51q0.4,1.4.7,2.75l0.59,2.69a0.26,0.26,0,0,0,.19.25,0.38,0.38,0,0,0,.28-0.27q0.13-.27.29-0.65c0.1-.26.2-0.53,0.3-0.83s0.19-.6.28-0.92,0.18-.63.25-1q0.23-1,.38-1.78l0-.29a0.61,0.61,0,0,1,.72-0.41,1.8,1.8,0,0,1,.55.05,0.94,0.94,0,0,1,.43.57q0.17,0.46.37,1.12t0.38,1.37q0.46,1.81.68,2.79a0.2,0.2,0,0,0,.14.2,0.58,0.58,0,0,0,.33-0.36,6.07,6.07,0,0,0,.41-1,13,13,0,0,0,.37-1.37,16.3,16.3,0,0,0,.36-3,0.39,0.39,0,0,1,.3-0.43l0.14,0h0.79Z"/><path class="cls-3" d="M94.1,155a0.71,0.71,0,0,1-.75.85H92.88a0.4,0.4,0,0,1-.34-0.16,0.82,0.82,0,0,1-.14-0.45,1,1,0,0,0-.17-0.52,1.22,1.22,0,0,0-.4-0.38,2.41,2.41,0,0,0-1.22-.27,2.64,2.64,0,0,0-1.32.32,2.56,2.56,0,0,0-.9.85,4.79,4.79,0,0,0-.66,2.67q0,3,2,3.77a3.24,3.24,0,0,0,1.18.23,4.23,4.23,0,0,0,1.83-.38l0.38-.18a0.82,0.82,0,0,1,.33-0.08,0.37,0.37,0,0,1,.23.07,1.06,1.06,0,0,1,.22.33,1,1,0,0,1,.13.44,0.63,0.63,0,0,1-.13.37,1.36,1.36,0,0,1-.32.32,3.15,3.15,0,0,1-.81.36,7,7,0,0,1-2,.26,4.86,4.86,0,0,1-2.06-.42,4.28,4.28,0,0,1-1.52-1.16,5.92,5.92,0,0,1-1.26-3.94A5.57,5.57,0,0,1,87.16,154a4.55,4.55,0,0,1,3.6-1.48,4.37,4.37,0,0,1,2.23.56A2,2,0,0,1,94.1,155Z"/><path class="cls-3" d="M95.25,159.33a4.35,4.35,0,0,1,.3-1.63,3.79,3.79,0,0,1,.82-1.27,4.12,4.12,0,0,1,5.61,0,4.11,4.11,0,0,1,1,2.91,4,4,0,0,1-1.11,2.93,4,4,0,0,1-5.54,0A4.07,4.07,0,0,1,95.25,159.33Zm2.2-1.74a2.64,2.64,0,0,0-.37.86,4.16,4.16,0,0,0-.12,1,3.35,3.35,0,0,0,.13.95,2.52,2.52,0,0,0,.4.81,1.94,1.94,0,0,0,1.64.78,1.88,1.88,0,0,0,1.63-.77,3,3,0,0,0,.49-1.83,2.52,2.52,0,0,0-1.13-2.39,2.26,2.26,0,0,0-2,0A1.94,1.94,0,0,0,97.45,157.59Z"/><path class="cls-3" d="M106.69,161.33q0,0.48.34,0.49a0.87,0.87,0,0,0,.28-0.06,1.81,1.81,0,0,1,.38-0.12,0.28,0.28,0,0,1,.25.2l0.21,0.49a0.52,0.52,0,0,1,0,.19,0.6,0.6,0,0,1-.26.38,2.06,2.06,0,0,1-1.36.43q-1.53,0-1.53-2v-9.75a0.47,0.47,0,0,1,.11-0.31,0.33,0.33,0,0,1,.26-0.13h0.92a0.32,0.32,0,0,1,.26.13,0.49,0.49,0,0,1,.1.31v9.8Z"/><path class="cls-3" d="M111.36,161.33q0,0.48.34,0.49a0.87,0.87,0,0,0,.28-0.06,1.81,1.81,0,0,1,.38-0.12,0.28,0.28,0,0,1,.25.2l0.21,0.49a0.52,0.52,0,0,1,0,.19,0.6,0.6,0,0,1-.26.38,2.06,2.06,0,0,1-1.36.43q-1.53,0-1.53-2v-9.75a0.47,0.47,0,0,1,.11-0.31,0.33,0.33,0,0,1,.26-0.13H111a0.32,0.32,0,0,1,.26.13,0.49,0.49,0,0,1,.1.31v9.8Z"/><path class="cls-3" d="M115.51,159.74v0.13a2,2,0,0,0,.59,1.5,2.32,2.32,0,0,0,1.69.6,3.09,3.09,0,0,0,1.8-.58,0.51,0.51,0,0,1,.28-0.11,0.41,0.41,0,0,1,.38.25l0.18,0.38a0.71,0.71,0,0,1,.06.29,0.52,0.52,0,0,1-.14.33,1.45,1.45,0,0,1-.37.3,2.74,2.74,0,0,1-.52.24,5.46,5.46,0,0,1-.61.17,6.19,6.19,0,0,1-1.31.14,3.91,3.91,0,0,1-1.38-.26,3.36,3.36,0,0,1-1.2-.76,3.82,3.82,0,0,1-1.09-2.86q0-3.17,2.23-3.94a4.39,4.39,0,0,1,1.43-.24,4.35,4.35,0,0,1,1.25.16,2.57,2.57,0,0,1,.89.46,2.21,2.21,0,0,1,.79,1.81,2,2,0,0,1-.93,1.74,3.9,3.9,0,0,1-2.15.53A7.27,7.27,0,0,1,115.51,159.74Zm3.36-2q0-1-1.31-1a1.82,1.82,0,0,0-1.75.92,2.14,2.14,0,0,0-.23.79,3.34,3.34,0,0,0,1.5.29Q118.87,158.76,118.87,157.74Z"/><path class="cls-3" d="M127.67,158.55a0.49,0.49,0,0,1-.56-0.55,1.21,1.21,0,0,0-.39-1,1.31,1.31,0,0,0-.9-0.29,1.72,1.72,0,0,0-.93.23,1.93,1.93,0,0,0-.6.59,3.12,3.12,0,0,0-.45,1.71,2.48,2.48,0,0,0,1.13,2.45,2,2,0,0,0,.91.21,2.58,2.58,0,0,0,.82-0.11,4.26,4.26,0,0,0,.57-0.24l0.44-.24a0.66,0.66,0,0,1,.31-0.11,0.44,0.44,0,0,1,.39.23l0.15,0.25a0.69,0.69,0,0,1,.1.34,0.59,0.59,0,0,1-.14.35,1.46,1.46,0,0,1-.37.33,3.2,3.2,0,0,1-.52.27,5.39,5.39,0,0,1-.61.21,4.14,4.14,0,0,1-2.75-.12,3.36,3.36,0,0,1-1.92-2.11,5.12,5.12,0,0,1-.26-1.68,4.72,4.72,0,0,1,.28-1.67,3.61,3.61,0,0,1,.77-1.25,3.53,3.53,0,0,1,2.66-1.05,3,3,0,0,1,2.22.76,2.21,2.21,0,0,1,.65,1.63,0.74,0.74,0,0,1-.85.83h-0.16Z"/><path class="cls-3" d="M134.86,156.54a0.36,0.36,0,0,1-.43.38H132.5v4.29a0.61,0.61,0,0,0,.69.7,2.36,2.36,0,0,0,1.14-.44,0.53,0.53,0,0,1,.3-0.09,0.3,0.3,0,0,1,.26.23L135,162a0.92,0.92,0,0,1,.09.35,0.5,0.5,0,0,1-.22.33,2.21,2.21,0,0,1-.55.34,3.4,3.4,0,0,1-1.37.34,3,3,0,0,1-1-.13,1.61,1.61,0,0,1-.61-0.38,2.05,2.05,0,0,1-.55-1.54v-4.36h-0.57a0.36,0.36,0,0,1-.43-0.39v-0.39a0.35,0.35,0,0,1,.08-0.25,0.61,0.61,0,0,1,.32-0.14l0.68-.2,0.26-1.61a0.42,0.42,0,0,1,.37-0.45h0.57a0.32,0.32,0,0,1,.26.13,0.5,0.5,0,0,1,.1.32v1.54h1.93a0.36,0.36,0,0,1,.43.38v0.66Z"/><path class="cls-3" d="M137.34,154.18a1.09,1.09,0,0,1-.79-0.32,1.05,1.05,0,0,1-.33-0.79,1.1,1.1,0,0,1,1.12-1.11,1,1,0,0,1,1,.67,1.07,1.07,0,0,1,.09.44,1.05,1.05,0,0,1-.33.79A1.09,1.09,0,0,1,137.34,154.18Zm0.8,7.15c0,0.32.11,0.49,0.32,0.49a0.61,0.61,0,0,0,.25-0.06,1.17,1.17,0,0,1,.36-0.12,0.27,0.27,0,0,1,.23.2l0.21,0.48a0.52,0.52,0,0,1,.05.22,0.72,0.72,0,0,1-.51.55,2.06,2.06,0,0,1-.95.25,2.23,2.23,0,0,1-.75-0.1,1.1,1.1,0,0,1-.48-0.34,2.56,2.56,0,0,1-.4-1.6V156a0.48,0.48,0,0,1,.11-0.32,0.33,0.33,0,0,1,.26-0.13h0.92a0.32,0.32,0,0,1,.26.13,0.5,0.5,0,0,1,.1.32v5.38Z"/><path class="cls-3" d="M140.74,159.33a4.35,4.35,0,0,1,.3-1.63,3.78,3.78,0,0,1,.82-1.27,4.12,4.12,0,0,1,5.61,0,4.11,4.11,0,0,1,1,2.91,4,4,0,0,1-1.11,2.93,4,4,0,0,1-5.54,0A4.07,4.07,0,0,1,140.74,159.33Zm2.2-1.74a2.63,2.63,0,0,0-.37.86,4.13,4.13,0,0,0-.12,1,3.35,3.35,0,0,0,.13.95,2.52,2.52,0,0,0,.4.81,1.94,1.94,0,0,0,1.64.78,1.88,1.88,0,0,0,1.63-.77,3,3,0,0,0,.49-1.83A2.52,2.52,0,0,0,145.6,157a2.27,2.27,0,0,0-2,0A1.94,1.94,0,0,0,142.93,157.59Z"/><path class="cls-3" d="M150.51,156a0.43,0.43,0,0,1,.47-0.49h0.38a0.52,0.52,0,0,1,.52.29,5,5,0,0,1,.33,1.08,2.82,2.82,0,0,1,1.83-1.44,3.75,3.75,0,0,1,1-.1,2.33,2.33,0,0,1,1.06.24,1.93,1.93,0,0,1,.77.67,4.1,4.1,0,0,1,.52,2.28v4.25a0.45,0.45,0,0,1-.12.32,0.48,0.48,0,0,1-.36.13h-0.67a0.5,0.5,0,0,1-.38-0.13,0.45,0.45,0,0,1-.12-0.32v-4.25q0-1.75-1.29-1.75a1.9,1.9,0,0,0-1.37.63,2.18,2.18,0,0,0-.66,1.57v3.8a0.47,0.47,0,0,1-.12.32,0.45,0.45,0,0,1-.36.13h-0.7a0.47,0.47,0,0,1-.36-0.13,0.45,0.45,0,0,1-.12-0.32v-4.2a14,14,0,0,0-.09-1.92A6.36,6.36,0,0,1,150.51,156Z"/></g><g class="cls-6"><path d="M51.92,197.14a2.5,2.5,0,0,0,1.9.71,2.65,2.65,0,0,0,1.82-.53,2.59,2.59,0,0,0,.78-1.37,0.37,0.37,0,0,1,.13-0.22,0.36,0.36,0,0,1,.23-0.08h0.64a0.31,0.31,0,0,1,.31.31,3.13,3.13,0,0,1-.45,1.42A3.39,3.39,0,0,1,56,198.6a4.31,4.31,0,0,1-2.18.5,4.17,4.17,0,0,1-2.06-.49,3.46,3.46,0,0,1-1.36-1.35,4.27,4.27,0,0,1-.53-2q0-.42,0-1.25t0-1.25a4.26,4.26,0,0,1,.53-2,3.46,3.46,0,0,1,1.36-1.35,4.17,4.17,0,0,1,2.06-.49,4.3,4.3,0,0,1,2.18.5,3.39,3.39,0,0,1,1.29,1.24,3.13,3.13,0,0,1,.45,1.42,0.31,0.31,0,0,1-.31.31H56.78a0.36,0.36,0,0,1-.23-0.08,0.37,0.37,0,0,1-.13-0.22,2.6,2.6,0,0,0-.78-1.37,2.65,2.65,0,0,0-1.82-.53,2.5,2.5,0,0,0-1.9.71,2.77,2.77,0,0,0-.72,1.9q0,0.42,0,1.18t0,1.18A2.77,2.77,0,0,0,51.92,197.14Z"/><path d="M63.37,191.8a0.37,0.37,0,0,1,.11.27v0.45a0.38,0.38,0,0,1-.38.38H62.3a1.45,1.45,0,0,0-1.61,1.61v4.09a0.38,0.38,0,0,1-.38.38H59.79a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a1.75,1.75,0,0,1,.67-0.62,2.29,2.29,0,0,1,1-.2h0.73A0.37,0.37,0,0,1,63.37,191.8Z"/><path d="M69.79,192.5a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H65.46v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57A3.06,3.06,0,0,1,65.82,192a3.11,3.11,0,0,1,1.6-.41A3,3,0,0,1,69.79,192.5Zm-0.39,2.17v0a1.79,1.79,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0h3.93Z"/><path d="M76.67,194.36v-0.18a1.29,1.29,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.71,1.71,0,0,0-.87.19,2.11,2.11,0,0,0-.59.57,0.46,0.46,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0H72.7a0.38,0.38,0,0,1-.27-0.11,0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.29,3.29,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.62,2.62,0,0,1,.8,2v4.27a0.38,0.38,0,0,1-.38.38H77.05a0.38,0.38,0,0,1-.38-0.38V198a1.64,1.64,0,0,1-1,.8,3.86,3.86,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.62,4.62,0,0,1,2.09-.81Zm0,1.12-1.65.27a3.77,3.77,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M82.06,197.44a1.11,1.11,0,0,0,.93.32h0.74a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H82.85a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1V189.4a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H81.81v3.63A1.5,1.5,0,0,0,82.06,197.44Z"/><path d="M90.74,192.5a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H86.42v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0H91a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57A3.06,3.06,0,0,1,86.77,192a3.11,3.11,0,0,1,1.6-.41A3,3,0,0,1,90.74,192.5Zm-0.39,2.17v0a1.79,1.79,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0h3.93Z"/><path d="M101,194.36v-0.18a1.29,1.29,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.71,1.71,0,0,0-.87.19,2.11,2.11,0,0,0-.59.57,0.46,0.46,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0H97.07a0.38,0.38,0,0,1-.27-0.11,0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.29,3.29,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.62,2.62,0,0,1,.8,2v4.27a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38V198a1.64,1.64,0,0,1-1,.8,3.86,3.86,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.62,4.62,0,0,1,2.09-.81Zm0,1.12-1.65.27a3.77,3.77,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M112.61,196.81a0.89,0.89,0,0,1,.2-0.29,0.4,0.4,0,0,1,.27-0.08h0.42a0.37,0.37,0,0,1,.27.11,0.32,0.32,0,0,1,.11.25,1.82,1.82,0,0,1-.38,1,3,3,0,0,1-1.09.92,3.54,3.54,0,0,1-1.68.38,3.38,3.38,0,0,1-1.72-.42,2.87,2.87,0,0,1-1.12-1.15,3.83,3.83,0,0,1-.43-1.65c0-.11,0-0.33,0-0.64s0-.38,0-0.48a3.77,3.77,0,0,1,1-2.32,3,3,0,0,1,2.31-.9,3.56,3.56,0,0,1,1.67.37,2.92,2.92,0,0,1,1.08.91,2,2,0,0,1,.4,1,0.31,0.31,0,0,1-.1.27,0.39,0.39,0,0,1-.27.11h-0.42a0.4,0.4,0,0,1-.27-0.08,0.88,0.88,0,0,1-.2-0.29,1.87,1.87,0,0,0-1.89-1.09,1.94,1.94,0,0,0-1.34.52,2.27,2.27,0,0,0-.66,1.57q0,0.15,0,.5c0,0.21,0,.37,0,0.48a2.33,2.33,0,0,0,.67,1.57,1.92,1.92,0,0,0,1.34.52A1.87,1.87,0,0,0,112.61,196.81Z"/><path d="M121.88,195.34q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C121.87,194.87,121.88,195.1,121.88,195.34Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,120,193.29Z"/><path d="M124.79,198.85a0.36,0.36,0,0,1-.27.11H124a0.38,0.38,0,0,1-.38-0.38V189.4A0.38,0.38,0,0,1,124,189h0.52a0.38,0.38,0,0,1,.38.38v9.18A0.36,0.36,0,0,1,124.79,198.85Z"/><path d="M128.16,198.85a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38V189.4a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v9.18A0.36,0.36,0,0,1,128.16,198.85Z"/><path d="M135.61,192.5a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38h-4.82v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.61,2.61,0,0,0,1.09-.18,2.24,2.24,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.06,3.06,0,0,1,1.11-1.11,3.11,3.11,0,0,1,1.6-.41A3,3,0,0,1,135.61,192.5Zm-0.39,2.17v0a1.79,1.79,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0h3.93Z"/><path d="M143,196.81a0.89,0.89,0,0,1,.2-0.29,0.39,0.39,0,0,1,.27-0.08h0.42a0.37,0.37,0,0,1,.27.11,0.32,0.32,0,0,1,.11.25,1.82,1.82,0,0,1-.38,1,3,3,0,0,1-1.09.92,3.54,3.54,0,0,1-1.68.38,3.39,3.39,0,0,1-1.72-.42,2.87,2.87,0,0,1-1.12-1.15,3.84,3.84,0,0,1-.43-1.65c0-.11,0-0.33,0-0.64s0-.38,0-0.48a3.77,3.77,0,0,1,1-2.32,3,3,0,0,1,2.31-.9,3.56,3.56,0,0,1,1.67.37,2.93,2.93,0,0,1,1.08.91,2,2,0,0,1,.4,1,0.31,0.31,0,0,1-.1.27,0.39,0.39,0,0,1-.27.11h-0.42a0.39,0.39,0,0,1-.27-0.08,0.88,0.88,0,0,1-.2-0.29,1.87,1.87,0,0,0-1.89-1.09,2,2,0,0,0-1.34.52,2.27,2.27,0,0,0-.66,1.57c0,0.1,0,.27,0,0.5s0,0.37,0,.48a2.33,2.33,0,0,0,.67,1.57,1.92,1.92,0,0,0,1.34.52A1.87,1.87,0,0,0,143,196.81Z"/><path d="M148.09,197.44a1.11,1.11,0,0,0,.93.32h0.74a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-0.88a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1V189.4A0.38,0.38,0,0,1,147,189h0.52a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-1.78v3.63A1.5,1.5,0,0,0,148.09,197.44Z"/><path d="M152.85,190.47a0.36,0.36,0,0,1-.27.11h-0.8a0.38,0.38,0,0,1-.38-0.38v-0.8a0.38,0.38,0,0,1,.38-0.38h0.8a0.38,0.38,0,0,1,.38.38v0.8A0.36,0.36,0,0,1,152.85,190.47Zm-0.14,8.39a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v6.52A0.36,0.36,0,0,1,152.71,198.85Z"/><path d="M161.25,195.34q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C161.24,194.87,161.25,195.1,161.25,195.34Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.37,2.37,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.37,2.37,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.37,2.37,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.37,2.37,0,0,0,159.35,193.29Z"/><path d="M169.35,198.85a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-3.7a2.24,2.24,0,0,0-.52-1.6,2.17,2.17,0,0,0-2.88,0,2.21,2.21,0,0,0-.53,1.6v3.7a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.41,2.41,0,0,1,.85-0.68,2.91,2.91,0,0,1,1.32-.27,3.2,3.2,0,0,1,1.57.38,2.67,2.67,0,0,1,1.07,1.08,3.45,3.45,0,0,1,.39,1.67v3.91A0.36,0.36,0,0,1,169.35,198.85Z"/><path d="M181.24,195.34q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C181.23,194.87,181.24,195.1,181.24,195.34Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.37,2.37,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.37,2.37,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.37,2.37,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.37,2.37,0,0,0,179.34,193.29Z"/><path d="M186.7,191.68a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-1.78v5.7a0.38,0.38,0,0,1-.38.38H184a0.38,0.38,0,0,1-.38-0.38v-5.7h-1.18a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1.18V191a2.52,2.52,0,0,1,.56-1.79,2.32,2.32,0,0,1,1.75-.59h0.88a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H186.1a1.11,1.11,0,0,0-.93.32,1.5,1.5,0,0,0-.24.92v0.63h1.78Z"/></g><g class="cls-6"><path d="M55.31,209.3a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H51v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.06,3.06,0,0,1,1.11-1.11,3.11,3.11,0,0,1,1.6-.41A3,3,0,0,1,55.31,209.3Zm-0.39,2.17v0a1.79,1.79,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0h3.93Z"/><path d="M58.5,215.5a1.49,1.49,0,0,1-.2.19,0.39,0.39,0,0,1-.25.08H57.41a0.32,0.32,0,0,1-.24-0.1,0.31,0.31,0,0,1-.1-0.22,0.34,0.34,0,0,1,.06-0.2L59.69,212l-2.42-3a0.34,0.34,0,0,1-.06-0.2,0.31,0.31,0,0,1,.1-0.22,0.32,0.32,0,0,1,.24-0.1h0.67a0.4,0.4,0,0,1,.25.08,1.5,1.5,0,0,1,.2.19l1.86,2.34,1.85-2.34a1.47,1.47,0,0,1,.2-0.19,0.4,0.4,0,0,1,.25-0.08h0.64a0.32,0.32,0,0,1,.24.1,0.31,0.31,0,0,1,.1.22,0.35,0.35,0,0,1-.06.2l-2.44,3.07,2.53,3.18a0.34,0.34,0,0,1,.06.2,0.31,0.31,0,0,1-.1.22,0.32,0.32,0,0,1-.24.1H62.9a0.39,0.39,0,0,1-.25-0.08,1.43,1.43,0,0,1-.2-0.19l-2-2.48Z"/><path d="M66.66,218a0.38,0.38,0,0,1-.38.38H65.77a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.54,2.54,0,0,1,2.17-1,3.11,3.11,0,0,1,1.57.41,3,3,0,0,1,1.13,1.16,3.86,3.86,0,0,1,.47,1.76q0,0.14,0,.45c0,0.21,0,.37,0,0.46a3.81,3.81,0,0,1-.47,1.76,3.06,3.06,0,0,1-1.13,1.16,3.1,3.1,0,0,1-1.58.41,2.91,2.91,0,0,1-1.32-.27,2.4,2.4,0,0,1-.85-0.68V218Zm0.59-3.89a2,2,0,0,0,1.44.55,1.87,1.87,0,0,0,1.45-.59,2.49,2.49,0,0,0,.59-1.6c0-.09,0-0.22,0-0.39s0-.3,0-0.39a2.49,2.49,0,0,0-.59-1.6,1.87,1.87,0,0,0-1.45-.59,2,2,0,0,0-1.43.54,2,2,0,0,0-.6,1.43v1.11A2.2,2.2,0,0,0,67.25,214.15Z"/><path d="M74.92,215.65a0.36,0.36,0,0,1-.27.11H74.14a0.38,0.38,0,0,1-.38-0.38V206.2a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v9.18A0.36,0.36,0,0,1,74.92,215.65Z"/><path d="M83.46,212.14q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C83.45,211.67,83.46,211.9,83.46,212.14Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,81.57,210.09Z"/><path d="M89.18,208.6a0.37,0.37,0,0,1,.11.27v0.45a0.38,0.38,0,0,1-.38.38H88.1a1.45,1.45,0,0,0-1.61,1.61v4.09a0.38,0.38,0,0,1-.38.38H85.59a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a1.75,1.75,0,0,1,.67-0.62,2.29,2.29,0,0,1,1-.2h0.73A0.37,0.37,0,0,1,89.18,208.6Z"/><path d="M94.72,211.16V211a1.29,1.29,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.71,1.71,0,0,0-.87.19,2.11,2.11,0,0,0-.59.57,0.46,0.46,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0H90.74a0.38,0.38,0,0,1-.27-0.11,0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.29,3.29,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.62,2.62,0,0,1,.8,2v4.27a0.38,0.38,0,0,1-.38.38H95.1a0.38,0.38,0,0,1-.38-0.38v-0.57a1.64,1.64,0,0,1-1,.8,3.86,3.86,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.62,4.62,0,0,1,2.09-.81Zm0,1.12-1.65.27a3.77,3.77,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M100.1,214.24a1.11,1.11,0,0,0,.93.32h0.74a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-0.88a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38v-0.45a0.38,0.38,0,0,1,.38-0.38h1V206.2a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H99.86v3.63A1.5,1.5,0,0,0,100.1,214.24Z"/><path d="M104.86,207.27a0.36,0.36,0,0,1-.27.11h-0.8a0.38,0.38,0,0,1-.38-0.38v-0.8a0.38,0.38,0,0,1,.38-0.38h0.8a0.38,0.38,0,0,1,.38.38V207A0.36,0.36,0,0,1,104.86,207.27Zm-0.14,8.39a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v6.52A0.36,0.36,0,0,1,104.72,215.65Z"/><path d="M113.26,212.14q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C113.25,211.67,113.26,211.9,113.26,212.14Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,111.36,210.09Z"/><path d="M121.36,215.65a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-3.7a2.23,2.23,0,0,0-.52-1.6,2.17,2.17,0,0,0-2.88,0,2.21,2.21,0,0,0-.52,1.6v3.7a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38v-6.52a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.41,2.41,0,0,1,.85-0.68,2.91,2.91,0,0,1,1.32-.27,3.2,3.2,0,0,1,1.57.38,2.67,2.67,0,0,1,1.07,1.08,3.45,3.45,0,0,1,.39,1.67v3.91A0.36,0.36,0,0,1,121.36,215.65Z"/><path d="M123.21,210.44a1.89,1.89,0,0,1,.31-1,2.14,2.14,0,0,1,.92-0.76,3.48,3.48,0,0,1,1.49-.29,4,4,0,0,1,1.5.25,2.46,2.46,0,0,1,1,.63,1.14,1.14,0,0,1,.33.73,0.32,0.32,0,0,1-.11.24,0.38,0.38,0,0,1-.27.11h-0.46a0.5,0.5,0,0,1-.42-0.2,1.82,1.82,0,0,0-1.53-.56,2,2,0,0,0-1,.24,0.73,0.73,0,0,0-.4.66,0.69,0.69,0,0,0,.2.53,1.44,1.44,0,0,0,.59.29q0.39,0.11,1.39.34a3.37,3.37,0,0,1,1.75.81,1.86,1.86,0,0,1,.52,1.33,1.77,1.77,0,0,1-.36,1.06,2.5,2.5,0,0,1-1,.79,3.71,3.71,0,0,1-1.55.3,4,4,0,0,1-1.53-.27,2.87,2.87,0,0,1-1-.67,1.13,1.13,0,0,1-.36-0.74,0.33,0.33,0,0,1,.11-0.25,0.37,0.37,0,0,1,.27-0.1H124a0.82,0.82,0,0,1,.22,0,0.35,0.35,0,0,1,.18.15,1.88,1.88,0,0,0,1.64.64,2.36,2.36,0,0,0,1.15-.27,0.79,0.79,0,0,0,.5-0.68,0.66,0.66,0,0,0-.24-0.54,2,2,0,0,0-.71-0.34q-0.46-.13-1.44-0.34Q123.21,212.1,123.21,210.44Z"/></g><rect class="cls-2" x="46.6" y="32.5" width="187.9" height="225.7"/><rect class="cls-3" x="48.7" y="15.7" width="200" height="240"/><rect class="cls-4" x="48.6" y="15.7" width="200" height="144"/><rect class="cls-5" x="112.8" y="45.6" width="74.4" height="74.4" transform="translate(-0.64 1.18) rotate(-0.45)"/><path class="cls-3" d="M149.8,51.1a31.7,31.7,0,1,0,31.9,31.5A31.77,31.77,0,0,0,149.8,51.1Zm-0.2,8h0.8a2.17,2.17,0,0,1,1,1.9v3.6c0,1.1-.5,1.8-1.4,1.8s-1.3-.6-1.3-1.8,0.1-2.5,0-3.7A1.75,1.75,0,0,1,149.6,59.1ZM129.4,71.6c0.9-1,1.3-1.1,2.5-.5a17.5,17.5,0,0,1,3.4,1.5,1.55,1.55,0,0,1,1.1,1.8,1.48,1.48,0,0,1-2.1.7c-1.4-.6-2.8-1.3-4.1-1.9a6,6,0,0,1-.8-0.8V71.6Zm6.4,13.1c-1.5.3-3,.4-4.4,0.7a1.83,1.83,0,0,1-1.8-.9V83.9a2,2,0,0,1,1.9-1.1,28.68,28.68,0,0,1,3.1-.5c0.3,0,.6-0.1,1-0.1a1.39,1.39,0,0,1,1.6,1.1C137.3,84,136.5,84.5,135.8,84.7Zm1.8-22.5a1.76,1.76,0,0,1,1.6.1c1.1,1.4,2,3,3,4.4a2.18,2.18,0,0,1,.1.6,2.28,2.28,0,0,1-.8.9,1.21,1.21,0,0,1-1.3-.1c-1.1-1.4-2-2.8-3-4.3A1,1,0,0,1,137.6,62.2Zm16.3,42.6h-8.2a1,1,0,0,1-1.2-1c-0.1-2.1-.1-2.1,2-2.1,2.7,0,5.3-.1,7.9,0,1.2,0,1.3,0,1.4,1.2C155.9,104.8,155.9,104.8,153.9,104.8Zm0-4.3h-8.2a1,1,0,0,1-1.2-1c-0.1-2.1-.1-2.1,2-2.1,2.7,0,5.3-.1,7.9,0,1.2,0,1.3,0,1.4,1.2C155.9,100.4,155.9,100.4,153.9,100.5Zm7.3-21.8a15.27,15.27,0,0,1-1.6,6,36.08,36.08,0,0,0-3.7,9.4c-0.2,1.3-.4,1.4-1.8,1.4h-8.2a1.13,1.13,0,0,1-1.3-.9c-0.2-.8-0.5-1.5-0.6-2.2a32.18,32.18,0,0,0-3.1-7.4,13.9,13.9,0,0,1-1.8-8,8.57,8.57,0,0,1,6.1-7.8,15,15,0,0,1,9.6-.1A9.33,9.33,0,0,1,161.2,78.7Zm1.4-14.8a28.16,28.16,0,0,1-2.4,3.6,1.34,1.34,0,0,1-2.1.4c-0.7-.4-0.8-1.2-0.3-2.1a26.37,26.37,0,0,1,2.3-3.5,2.06,2.06,0,0,1,1.7-.7c0.4,0,.7.7,1,1A2,2,0,0,1,162.6,63.9Zm8.1,20.2a1.68,1.68,0,0,1-2.1,1l-4.1-.6a1.46,1.46,0,0,1-1.3-1.6,1.29,1.29,0,0,1,1.5-1.2c1.6,0.2,3.2.5,4.6,0.8a4.7,4.7,0,0,1,1.2.9A1.08,1.08,0,0,1,170.7,84.1Zm-0.1-12a7.41,7.41,0,0,1-1.3,1.2c-1,.6-2.1,1-3.2,1.6a1.63,1.63,0,0,1-2.4-.6c-0.3-.7.1-1.5,1.1-2s2.3-.9,3.5-1.5,1.6-.4,2.4.7A1.27,1.27,0,0,0,170.6,72.1Z"/><g class="cls-6"><path class="cls-3" d="M64.88,136.82a0.7,0.7,0,0,1,.74-0.85H65.9a0.78,0.78,0,0,1,.39.07,1.25,1.25,0,0,1,.38.39l4.4,6.48a4.86,4.86,0,0,1,.4.68l0.28,0.59h0a0.36,0.36,0,0,0,0-.18q0-.18-0.07-0.52a4.18,4.18,0,0,1-.05-0.67v-6.43a0.43,0.43,0,0,1,.11-0.3A0.33,0.33,0,0,1,72,136h0.9a0.32,0.32,0,0,1,.25.12,0.43,0.43,0,0,1,.11.3v9.38a0.54,0.54,0,0,1-.17.42,0.82,0.82,0,0,1-.52.26H72.17a0.89,0.89,0,0,1-.5-0.11,3.12,3.12,0,0,1-.57-0.67q-3.9-5.71-4.18-6.17l-0.57-1,0,0s0,0,0,.1,0,0.18.06,0.34,0,0.34.07,0.54q0.07,0.68.07,1.27V146a0.43,0.43,0,0,1-.11.29,0.33,0.33,0,0,1-.26.12h-0.9a0.32,0.32,0,0,1-.25-0.12,0.43,0.43,0,0,1-.11-0.29v-9.21Z"/><path class="cls-3" d="M77.26,143v0.13a2,2,0,0,0,.59,1.5,2.32,2.32,0,0,0,1.69.6,3.09,3.09,0,0,0,1.8-.58,0.51,0.51,0,0,1,.28-0.11,0.41,0.41,0,0,1,.38.25l0.18,0.38a0.71,0.71,0,0,1,.06.29,0.52,0.52,0,0,1-.14.33,1.45,1.45,0,0,1-.37.3,2.74,2.74,0,0,1-.52.24,5.46,5.46,0,0,1-.61.17,6.19,6.19,0,0,1-1.31.14,3.91,3.91,0,0,1-1.38-.26,3.36,3.36,0,0,1-1.2-.76,3.82,3.82,0,0,1-1.09-2.86q0-3.17,2.23-3.94a4.39,4.39,0,0,1,1.43-.24,4.35,4.35,0,0,1,1.25.16,2.57,2.57,0,0,1,.89.46,2.21,2.21,0,0,1,.79,1.81,2,2,0,0,1-.93,1.74,3.9,3.9,0,0,1-2.15.53A7.27,7.27,0,0,1,77.26,143Zm3.36-2q0-1-1.31-1a1.82,1.82,0,0,0-1.75.92,2.14,2.14,0,0,0-.23.79,3.34,3.34,0,0,0,1.5.29Q80.63,142,80.63,141Z"/><path class="cls-3" d="M94.62,138.74a0.38,0.38,0,0,1,.44.41A14.59,14.59,0,0,1,94.4,143a8.37,8.37,0,0,1-1.32,2.8,1.51,1.51,0,0,1-1.18.7,1.42,1.42,0,0,1-1-.31,1.68,1.68,0,0,1-.48-0.9q-0.37-1.54-.52-2.1l-0.26-1-0.17-.59,0,0.18-0.06.27-0.14.59a16.89,16.89,0,0,1-.63,1.95q-0.22.5-.38,0.78a3,3,0,0,1-.37.52,1.43,1.43,0,0,1-1.08.55,1.47,1.47,0,0,1-1.53-1.31q-0.35-1.47-.79-2.91l-0.91-2.88a0.83,0.83,0,0,1,0-.27,0.39,0.39,0,0,1,.46-0.42h0.81a0.59,0.59,0,0,1,.56.51q0.4,1.4.7,2.75l0.59,2.69a0.26,0.26,0,0,0,.19.25,0.38,0.38,0,0,0,.28-0.27q0.13-.27.29-0.65c0.1-.26.2-0.53,0.3-0.83s0.19-.6.28-0.92,0.18-.63.25-1q0.23-1,.38-1.78l0-.29a0.61,0.61,0,0,1,.72-0.41,1.8,1.8,0,0,1,.55.05,0.94,0.94,0,0,1,.43.57q0.17,0.46.37,1.12T91.1,142q0.46,1.81.68,2.79a0.2,0.2,0,0,0,.14.2,0.58,0.58,0,0,0,.33-0.36,6.07,6.07,0,0,0,.41-1,13,13,0,0,0,.37-1.37,16.3,16.3,0,0,0,.36-3,0.39,0.39,0,0,1,.3-0.43l0.14,0h0.79Z"/><path class="cls-3" d="M108.31,138.2a0.71,0.71,0,0,1-.75.85h-0.47a0.4,0.4,0,0,1-.34-0.16,0.82,0.82,0,0,1-.14-0.45,1,1,0,0,0-.17-0.52,1.22,1.22,0,0,0-.4-0.38,2.41,2.41,0,0,0-1.22-.27,2.64,2.64,0,0,0-1.32.32,2.56,2.56,0,0,0-.9.85,4.79,4.79,0,0,0-.66,2.67q0,3,2,3.77a3.24,3.24,0,0,0,1.18.23,4.23,4.23,0,0,0,1.83-.38l0.38-.18a0.82,0.82,0,0,1,.33-0.08,0.37,0.37,0,0,1,.23.07,1.06,1.06,0,0,1,.22.33,1,1,0,0,1,.13.44,0.63,0.63,0,0,1-.13.37,1.36,1.36,0,0,1-.32.32,3.15,3.15,0,0,1-.81.36,7,7,0,0,1-2,.26,4.86,4.86,0,0,1-2.06-.42,4.28,4.28,0,0,1-1.52-1.16,5.92,5.92,0,0,1-1.26-3.94,5.56,5.56,0,0,1,1.28-3.84,4.55,4.55,0,0,1,3.6-1.48,4.37,4.37,0,0,1,2.23.56A2,2,0,0,1,108.31,138.2Z"/><path class="cls-3" d="M109.46,142.57a4.35,4.35,0,0,1,.3-1.63,3.79,3.79,0,0,1,.82-1.27,4.13,4.13,0,0,1,5.61,0,4.11,4.11,0,0,1,1,2.91,4,4,0,0,1-1.11,2.93,4,4,0,0,1-5.54,0A4.07,4.07,0,0,1,109.46,142.57Zm2.2-1.74a2.64,2.64,0,0,0-.37.86,4.16,4.16,0,0,0-.12,1,3.35,3.35,0,0,0,.13.95,2.52,2.52,0,0,0,.4.81,1.94,1.94,0,0,0,1.64.78,1.88,1.88,0,0,0,1.63-.77,3,3,0,0,0,.49-1.83,2.52,2.52,0,0,0-1.13-2.39,2.26,2.26,0,0,0-2,0A1.94,1.94,0,0,0,111.65,140.83Z"/><path class="cls-3" d="M120.9,144.58q0,0.48.34,0.49a0.87,0.87,0,0,0,.28-0.06,1.81,1.81,0,0,1,.38-0.12,0.28,0.28,0,0,1,.25.2l0.21,0.49a0.52,0.52,0,0,1,0,.19,0.6,0.6,0,0,1-.26.38,2.06,2.06,0,0,1-1.36.43q-1.53,0-1.53-2v-9.75a0.47,0.47,0,0,1,.11-0.31,0.33,0.33,0,0,1,.26-0.13h0.92a0.32,0.32,0,0,1,.26.13,0.49,0.49,0,0,1,.1.31v9.8Z"/><path class="cls-3" d="M125.57,144.58q0,0.48.34,0.49a0.87,0.87,0,0,0,.28-0.06,1.81,1.81,0,0,1,.38-0.12,0.28,0.28,0,0,1,.25.2l0.21,0.49a0.52,0.52,0,0,1,0,.19,0.6,0.6,0,0,1-.26.38,2.06,2.06,0,0,1-1.36.43q-1.53,0-1.53-2v-9.75a0.47,0.47,0,0,1,.11-0.31,0.33,0.33,0,0,1,.26-0.13h0.92a0.32,0.32,0,0,1,.26.13,0.49,0.49,0,0,1,.1.31v9.8Z"/><path class="cls-3" d="M129.72,143v0.13a2,2,0,0,0,.59,1.5,2.32,2.32,0,0,0,1.69.6,3.09,3.09,0,0,0,1.8-.58,0.51,0.51,0,0,1,.28-0.11,0.41,0.41,0,0,1,.38.25l0.18,0.38a0.71,0.71,0,0,1,.06.29,0.52,0.52,0,0,1-.14.33,1.44,1.44,0,0,1-.37.3,2.74,2.74,0,0,1-.52.24,5.46,5.46,0,0,1-.61.17,6.19,6.19,0,0,1-1.31.14,3.91,3.91,0,0,1-1.38-.26,3.36,3.36,0,0,1-1.2-.76,3.82,3.82,0,0,1-1.09-2.86q0-3.17,2.23-3.94a4.39,4.39,0,0,1,1.43-.24,4.35,4.35,0,0,1,1.25.16,2.57,2.57,0,0,1,.89.46,2.21,2.21,0,0,1,.79,1.81,2,2,0,0,1-.93,1.74,3.9,3.9,0,0,1-2.15.53A7.27,7.27,0,0,1,129.72,143Zm3.36-2q0-1-1.31-1a1.82,1.82,0,0,0-1.75.92,2.14,2.14,0,0,0-.23.79,3.34,3.34,0,0,0,1.5.29Q133.08,142,133.08,141Z"/><path class="cls-3" d="M141.87,141.79a0.49,0.49,0,0,1-.56-0.55,1.21,1.21,0,0,0-.39-1A1.31,1.31,0,0,0,140,140a1.72,1.72,0,0,0-.93.23,1.92,1.92,0,0,0-.6.59,3.12,3.12,0,0,0-.45,1.71,2.48,2.48,0,0,0,1.13,2.45,2,2,0,0,0,.91.21,2.58,2.58,0,0,0,.82-0.11,4.31,4.31,0,0,0,.57-0.24l0.44-.24a0.66,0.66,0,0,1,.31-0.11,0.44,0.44,0,0,1,.39.23l0.15,0.25a0.69,0.69,0,0,1,.1.34,0.59,0.59,0,0,1-.14.35,1.46,1.46,0,0,1-.37.33,3.19,3.19,0,0,1-.52.27,5.45,5.45,0,0,1-.61.21,4.14,4.14,0,0,1-2.75-.12,3.36,3.36,0,0,1-1.92-2.11,5.11,5.11,0,0,1-.26-1.68,4.72,4.72,0,0,1,.28-1.67,3.61,3.61,0,0,1,.77-1.25,3.53,3.53,0,0,1,2.66-1.05,3,3,0,0,1,2.22.76,2.21,2.21,0,0,1,.65,1.63,0.74,0.74,0,0,1-.85.83h-0.16Z"/><path class="cls-3" d="M149.07,139.78a0.36,0.36,0,0,1-.43.38h-1.93v4.29a0.61,0.61,0,0,0,.69.7,2.36,2.36,0,0,0,1.14-.44,0.53,0.53,0,0,1,.3-0.09,0.3,0.3,0,0,1,.26.23l0.16,0.36a0.92,0.92,0,0,1,.09.35,0.5,0.5,0,0,1-.22.33,2.21,2.21,0,0,1-.55.34,3.4,3.4,0,0,1-1.37.34,3,3,0,0,1-1-.13,1.6,1.6,0,0,1-.61-0.38,2,2,0,0,1-.55-1.54v-4.36H144.5a0.36,0.36,0,0,1-.43-0.39v-0.39a0.35,0.35,0,0,1,.08-0.25,0.6,0.6,0,0,1,.32-0.14l0.68-.2,0.26-1.61a0.42,0.42,0,0,1,.37-0.45h0.57a0.32,0.32,0,0,1,.26.13,0.5,0.5,0,0,1,.1.32v1.54h1.93a0.36,0.36,0,0,1,.43.38v0.66Z"/><path class="cls-3" d="M151.54,137.43a1.09,1.09,0,0,1-.79-0.32,1.05,1.05,0,0,1-.33-0.79,1.1,1.1,0,0,1,1.12-1.11,1,1,0,0,1,1,.67,1.07,1.07,0,0,1,.09.44,1.05,1.05,0,0,1-.33.79A1.09,1.09,0,0,1,151.54,137.43Zm0.8,7.15c0,0.32.11,0.49,0.32,0.49a0.61,0.61,0,0,0,.25-0.06,1.17,1.17,0,0,1,.36-0.12,0.27,0.27,0,0,1,.23.2l0.21,0.48a0.52,0.52,0,0,1,.05.22,0.72,0.72,0,0,1-.51.55,2.06,2.06,0,0,1-.95.25,2.23,2.23,0,0,1-.75-0.1,1.1,1.1,0,0,1-.48-0.34,2.56,2.56,0,0,1-.4-1.6V139.2a0.48,0.48,0,0,1,.11-0.32,0.33,0.33,0,0,1,.26-0.13H152a0.32,0.32,0,0,1,.26.13,0.5,0.5,0,0,1,.1.32v5.38Z"/><path class="cls-3" d="M154.94,142.57a4.35,4.35,0,0,1,.3-1.63,3.78,3.78,0,0,1,.82-1.27,4.13,4.13,0,0,1,5.61,0,4.11,4.11,0,0,1,1,2.91,4,4,0,0,1-1.11,2.93,4,4,0,0,1-5.54,0A4.07,4.07,0,0,1,154.94,142.57Zm2.2-1.74a2.63,2.63,0,0,0-.37.86,4.13,4.13,0,0,0-.12,1,3.35,3.35,0,0,0,.13.95,2.52,2.52,0,0,0,.4.81,1.94,1.94,0,0,0,1.64.78,1.88,1.88,0,0,0,1.63-.77,3,3,0,0,0,.49-1.83,2.52,2.52,0,0,0-1.13-2.39,2.27,2.27,0,0,0-2,0A1.94,1.94,0,0,0,157.14,140.83Z"/><path class="cls-3" d="M164.72,139.24a0.43,0.43,0,0,1,.47-0.49h0.38a0.52,0.52,0,0,1,.52.29,5,5,0,0,1,.33,1.08,2.82,2.82,0,0,1,1.83-1.44,3.75,3.75,0,0,1,1-.1,2.33,2.33,0,0,1,1.06.24,1.94,1.94,0,0,1,.77.67,4.1,4.1,0,0,1,.52,2.28V146a0.45,0.45,0,0,1-.12.32,0.48,0.48,0,0,1-.36.13h-0.67a0.5,0.5,0,0,1-.38-0.13,0.45,0.45,0,0,1-.12-0.32v-4.25q0-1.75-1.29-1.75a1.9,1.9,0,0,0-1.37.63,2.18,2.18,0,0,0-.66,1.57V146a0.47,0.47,0,0,1-.12.32,0.45,0.45,0,0,1-.36.13h-0.7a0.47,0.47,0,0,1-.36-0.13,0.45,0.45,0,0,1-.12-0.32v-4.2a14,14,0,0,0-.09-1.92A6.38,6.38,0,0,1,164.72,139.24Z"/></g><g class="cls-6"><path d="M66.12,180.38a2.51,2.51,0,0,0,1.9.71,2.65,2.65,0,0,0,1.82-.53,2.58,2.58,0,0,0,.78-1.37,0.38,0.38,0,0,1,.36-0.31h0.64a0.31,0.31,0,0,1,.31.31,3.13,3.13,0,0,1-.45,1.42,3.4,3.4,0,0,1-1.29,1.24,4.31,4.31,0,0,1-2.18.5,4.17,4.17,0,0,1-2.06-.49,3.45,3.45,0,0,1-1.36-1.35,4.27,4.27,0,0,1-.53-2q0-.42,0-1.25t0-1.25a4.26,4.26,0,0,1,.53-2A3.45,3.45,0,0,1,66,172.76a4.17,4.17,0,0,1,2.06-.49,4.31,4.31,0,0,1,2.18.5A3.4,3.4,0,0,1,71.49,174a3.13,3.13,0,0,1,.45,1.42,0.31,0.31,0,0,1-.31.31H71a0.38,0.38,0,0,1-.36-0.31,2.6,2.6,0,0,0-.78-1.37,2.66,2.66,0,0,0-1.82-.53,2.51,2.51,0,0,0-1.9.71,2.77,2.77,0,0,0-.72,1.9c0,0.28,0,.67,0,1.18s0,0.9,0,1.18A2.77,2.77,0,0,0,66.12,180.38Z"/><path d="M77.58,175a0.37,0.37,0,0,1,.11.27v0.45a0.38,0.38,0,0,1-.38.38H76.5a1.45,1.45,0,0,0-1.61,1.61v4.09a0.38,0.38,0,0,1-.38.38H74a0.38,0.38,0,0,1-.38-0.38V175.3a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.44a1.74,1.74,0,0,1,.67-0.62,2.29,2.29,0,0,1,1-.2h0.73A0.37,0.37,0,0,1,77.58,175Z"/><path d="M84,175.74a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H79.67v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.22,2.22,0,0,0,.69-0.52,0.78,0.78,0,0,1,.2-0.15,0.6,0.6,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.74,2.74,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57A3.07,3.07,0,0,1,80,175.19a3.12,3.12,0,0,1,1.6-.41A3,3,0,0,1,84,175.74Zm-0.39,2.17v0a1.8,1.8,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.79,1.79,0,0,0-.58,1.36v0H83.6Z"/><path d="M90.88,177.6v-0.18a1.28,1.28,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.72,1.72,0,0,0-.87.19,2.13,2.13,0,0,0-.59.57,0.48,0.48,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0H86.9a0.39,0.39,0,0,1-.27-0.11,0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.31,3.31,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.61,2.61,0,0,1,.8,2v4.27a0.38,0.38,0,0,1-.38.38H91.26a0.38,0.38,0,0,1-.38-0.38v-0.57a1.63,1.63,0,0,1-1,.8,3.84,3.84,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.63,4.63,0,0,1,2.09-.8Zm0,1.12-1.65.27a3.74,3.74,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M96.26,180.68a1.11,1.11,0,0,0,.93.32h0.74a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H97.05a2.24,2.24,0,0,1-1.75-.62,2.64,2.64,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38V175.3a0.38,0.38,0,0,1,.38-0.38h1v-2.28a0.38,0.38,0,0,1,.38-0.38h0.52a0.36,0.36,0,0,1,.27.11,0.37,0.37,0,0,1,.11.27v2.28H97.8a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H96v3.63A1.5,1.5,0,0,0,96.26,180.68Z"/><path d="M104.95,175.74a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38h-4.82v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.22,2.22,0,0,0,.69-0.52,0.78,0.78,0,0,1,.2-0.15,0.6,0.6,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.74,2.74,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.07,3.07,0,0,1,1.11-1.11,3.12,3.12,0,0,1,1.6-.41A3,3,0,0,1,104.95,175.74Zm-0.39,2.17v0a1.8,1.8,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.79,1.79,0,0,0-.58,1.36v0h3.93Z"/><path d="M115.25,177.6v-0.18a1.28,1.28,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.72,1.72,0,0,0-.87.19,2.13,2.13,0,0,0-.59.57,0.48,0.48,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0h-0.42a0.39,0.39,0,0,1-.27-0.11,0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.31,3.31,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.61,2.61,0,0,1,.8,2v4.27a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38v-0.57a1.63,1.63,0,0,1-1,.8,3.84,3.84,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.63,4.63,0,0,1,2.09-.8Zm0,1.12-1.65.27a3.74,3.74,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M126.82,180a0.9,0.9,0,0,1,.2-0.29,0.39,0.39,0,0,1,.27-0.08h0.42a0.37,0.37,0,0,1,.27.11,0.32,0.32,0,0,1,.11.24,1.82,1.82,0,0,1-.38,1,3,3,0,0,1-1.09.92,3.54,3.54,0,0,1-1.68.38,3.39,3.39,0,0,1-1.72-.42,2.87,2.87,0,0,1-1.12-1.15,3.83,3.83,0,0,1-.43-1.65c0-.11,0-0.33,0-0.64s0-.38,0-0.48a3.77,3.77,0,0,1,1-2.32,3,3,0,0,1,2.31-.9,3.57,3.57,0,0,1,1.67.37,2.93,2.93,0,0,1,1.08.91,2,2,0,0,1,.4,1,0.31,0.31,0,0,1-.1.27,0.39,0.39,0,0,1-.27.11h-0.42a0.39,0.39,0,0,1-.27-0.08,0.88,0.88,0,0,1-.2-0.29,1.87,1.87,0,0,0-1.89-1.09,1.94,1.94,0,0,0-1.34.52,2.26,2.26,0,0,0-.66,1.57q0,0.15,0,.5t0,0.47a2.33,2.33,0,0,0,.67,1.57,1.92,1.92,0,0,0,1.34.52A1.87,1.87,0,0,0,126.82,180Z"/><path d="M136.08,178.58c0,0.22,0,.44,0,0.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C136.08,178.11,136.08,178.34,136.08,178.58Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.37,2.37,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.59-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.37,2.37,0,0,0,134.19,176.54Z"/><path d="M139,182.09a0.37,0.37,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.37,0.37,0,0,1,.27.11,0.37,0.37,0,0,1,.11.27v9.18A0.37,0.37,0,0,1,139,182.09Z"/><path d="M142.37,182.09a0.37,0.37,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.37,0.37,0,0,1,.27.11,0.37,0.37,0,0,1,.11.27v9.18A0.37,0.37,0,0,1,142.37,182.09Z"/><path d="M149.82,175.74a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38h-4.82v0.08a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.22,2.22,0,0,0,.69-0.52,0.78,0.78,0,0,1,.2-0.15,0.61,0.61,0,0,1,.25,0h0.42a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.74,2.74,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57,3.07,3.07,0,0,1,1.11-1.11,3.12,3.12,0,0,1,1.6-.41A3,3,0,0,1,149.82,175.74Zm-0.39,2.17v0a1.8,1.8,0,0,0-.54-1.37,2,2,0,0,0-1.43-.51,2,2,0,0,0-1.38.52,1.79,1.79,0,0,0-.58,1.36v0h3.93Z"/><path d="M157.21,180a0.9,0.9,0,0,1,.2-0.29,0.39,0.39,0,0,1,.27-0.08h0.42a0.37,0.37,0,0,1,.27.11,0.32,0.32,0,0,1,.11.24,1.82,1.82,0,0,1-.38,1A3,3,0,0,1,157,182a3.54,3.54,0,0,1-1.68.38,3.4,3.4,0,0,1-1.72-.42,2.88,2.88,0,0,1-1.12-1.15,3.84,3.84,0,0,1-.43-1.65c0-.11,0-0.33,0-0.64s0-.38,0-0.48a3.77,3.77,0,0,1,1-2.32,3,3,0,0,1,2.31-.9,3.57,3.57,0,0,1,1.67.37,2.94,2.94,0,0,1,1.08.91,2,2,0,0,1,.4,1,0.31,0.31,0,0,1-.1.27,0.39,0.39,0,0,1-.27.11h-0.42a0.39,0.39,0,0,1-.27-0.08,0.88,0.88,0,0,1-.2-0.29,1.87,1.87,0,0,0-1.89-1.09,1.94,1.94,0,0,0-1.34.52,2.27,2.27,0,0,0-.66,1.57c0,0.1,0,.27,0,0.5s0,0.37,0,.47a2.33,2.33,0,0,0,.67,1.57,1.92,1.92,0,0,0,1.34.52A1.87,1.87,0,0,0,157.21,180Z"/><path d="M162.3,180.68a1.11,1.11,0,0,0,.93.32H164a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-0.88a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38V175.3a0.38,0.38,0,0,1,.38-0.38h1v-2.28a0.37,0.37,0,0,1,.11-0.27,0.37,0.37,0,0,1,.27-0.11h0.52a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-1.78v3.63A1.51,1.51,0,0,0,162.3,180.68Z"/><path d="M167.05,173.71a0.36,0.36,0,0,1-.27.11H166a0.38,0.38,0,0,1-.38-0.38v-0.8a0.37,0.37,0,0,1,.11-0.27,0.37,0.37,0,0,1,.27-0.11h0.8a0.38,0.38,0,0,1,.38.38v0.8A0.36,0.36,0,0,1,167.05,173.71Zm-0.14,8.39a0.37,0.37,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38V175.3a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v6.53A0.37,0.37,0,0,1,166.91,182.09Z"/><path d="M175.45,178.58c0,0.22,0,.44,0,0.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C175.44,178.11,175.45,178.34,175.45,178.58Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.37,2.37,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.37,2.37,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.37,2.37,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.37,2.37,0,0,0,173.56,176.54Z"/><path d="M183.56,182.09a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-3.7a2.24,2.24,0,0,0-.52-1.6,2.17,2.17,0,0,0-2.88,0,2.22,2.22,0,0,0-.53,1.6v3.7a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38V175.3a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.44a2.38,2.38,0,0,1,.85-0.68,2.91,2.91,0,0,1,1.32-.27,3.2,3.2,0,0,1,1.57.38,2.67,2.67,0,0,1,1.07,1.08,3.45,3.45,0,0,1,.39,1.67v3.91A0.37,0.37,0,0,1,183.56,182.09Z"/><path d="M195.44,178.58c0,0.22,0,.44,0,0.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C195.43,178.11,195.44,178.34,195.44,178.58Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.37,2.37,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.37,2.37,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.37,2.37,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.37,2.37,0,0,0,193.55,176.54Z"/><path d="M200.9,174.93a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-1.78v5.7a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38v-5.7h-1.18a0.38,0.38,0,0,1-.38-0.38V175.3a0.38,0.38,0,0,1,.38-0.38h1.18v-0.7a2.52,2.52,0,0,1,.56-1.79,2.32,2.32,0,0,1,1.75-.59H201a0.36,0.36,0,0,1,.27.11,0.37,0.37,0,0,1,.11.27v0.45a0.38,0.38,0,0,1-.38.38H200.3a1.11,1.11,0,0,0-.93.32,1.51,1.51,0,0,0-.24.92v0.63h1.78Z"/></g><g class="cls-6"><path d="M69.52,192.54a3.7,3.7,0,0,1,.87,2.59v0.38a0.38,0.38,0,0,1-.38.38H65.19V196a2,2,0,0,0,.57,1.45,1.91,1.91,0,0,0,1.39.52,2.6,2.6,0,0,0,1.08-.18,2.23,2.23,0,0,0,.69-0.52,0.74,0.74,0,0,1,.2-0.15,0.59,0.59,0,0,1,.25,0H69.8a0.38,0.38,0,0,1,.28.11,0.32,0.32,0,0,1,.1.27,1.53,1.53,0,0,1-.41.78,2.75,2.75,0,0,1-1,.67,4.18,4.18,0,0,1-1.59.27,3.08,3.08,0,0,1-1.6-.41,3,3,0,0,1-1.11-1.14,4,4,0,0,1-.5-1.64q0-.42,0-0.64t0-.64a3.76,3.76,0,0,1,.5-1.57A3.06,3.06,0,0,1,65.55,192a3.11,3.11,0,0,1,1.6-.41A3,3,0,0,1,69.52,192.54Zm-0.39,2.17v0a1.79,1.79,0,0,0-.54-1.37,2,2,0,0,0-1.44-.51,2,2,0,0,0-1.38.52,1.8,1.8,0,0,0-.58,1.36v0h3.93Z"/><path d="M72.71,198.74a1.49,1.49,0,0,1-.2.19,0.39,0.39,0,0,1-.25.08H71.62a0.32,0.32,0,0,1-.24-0.1,0.31,0.31,0,0,1-.1-0.22,0.34,0.34,0,0,1,.06-0.2l2.56-3.21-2.42-3a0.34,0.34,0,0,1-.06-0.2,0.31,0.31,0,0,1,.1-0.22,0.32,0.32,0,0,1,.24-0.1h0.67a0.4,0.4,0,0,1,.25.08,1.5,1.5,0,0,1,.2.19l1.86,2.34L76.59,192a1.47,1.47,0,0,1,.2-0.19,0.4,0.4,0,0,1,.25-0.08h0.64a0.32,0.32,0,0,1,.24.1,0.31,0.31,0,0,1,.1.22,0.35,0.35,0,0,1-.06.2l-2.44,3.07,2.53,3.18a0.34,0.34,0,0,1,.06.2,0.31,0.31,0,0,1-.1.22,0.32,0.32,0,0,1-.24.1H77.1a0.39,0.39,0,0,1-.25-0.08,1.43,1.43,0,0,1-.2-0.19l-2-2.48Z"/><path d="M80.87,201.29a0.38,0.38,0,0,1-.38.38H80a0.38,0.38,0,0,1-.38-0.38V192.1a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.54,2.54,0,0,1,2.17-1,3.11,3.11,0,0,1,1.57.41,3,3,0,0,1,1.13,1.16,3.86,3.86,0,0,1,.47,1.76q0,0.14,0,.45c0,0.21,0,.37,0,0.46a3.81,3.81,0,0,1-.47,1.76,3.06,3.06,0,0,1-1.13,1.16,3.1,3.1,0,0,1-1.58.41,2.91,2.91,0,0,1-1.32-.27,2.4,2.4,0,0,1-.85-0.68v3.09Zm0.59-3.89a2,2,0,0,0,1.44.55,1.87,1.87,0,0,0,1.45-.59,2.49,2.49,0,0,0,.59-1.6c0-.09,0-0.22,0-0.39s0-.3,0-0.39a2.49,2.49,0,0,0-.59-1.6,1.87,1.87,0,0,0-1.45-.59,2,2,0,0,0-1.43.54,2,2,0,0,0-.6,1.43v1.11A2.2,2.2,0,0,0,81.46,197.4Z"/><path d="M89.13,198.89a0.36,0.36,0,0,1-.27.11H88.35a0.38,0.38,0,0,1-.38-0.38v-9.18a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v9.18A0.36,0.36,0,0,1,89.13,198.89Z"/><path d="M97.67,195.38q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0A3.5,3.5,0,0,1,91,196c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C97.66,194.91,97.67,195.14,97.67,195.38Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,95.77,193.34Z"/><path d="M103.38,191.84a0.37,0.37,0,0,1,.11.27v0.45a0.38,0.38,0,0,1-.38.38H102.3a1.45,1.45,0,0,0-1.61,1.61v4.09a0.38,0.38,0,0,1-.38.38H99.8a0.38,0.38,0,0,1-.38-0.38V192.1a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a1.75,1.75,0,0,1,.67-0.62,2.29,2.29,0,0,1,1-.2h0.73A0.37,0.37,0,0,1,103.38,191.84Z"/><path d="M108.93,194.4v-0.18a1.29,1.29,0,0,0-.46-1.06,1.81,1.81,0,0,0-1.18-.37,1.71,1.71,0,0,0-.87.19,2.11,2.11,0,0,0-.59.57,0.46,0.46,0,0,1-.18.15,0.7,0.7,0,0,1-.27,0h-0.42a0.38,0.38,0,0,1-.27-0.11,0.31,0.31,0,0,1-.1-0.27,1.48,1.48,0,0,1,.41-0.83,2.79,2.79,0,0,1,1-.68,3.29,3.29,0,0,1,1.32-.27,3,3,0,0,1,2.11.73,2.62,2.62,0,0,1,.8,2v4.27a0.38,0.38,0,0,1-.38.38H109.3a0.38,0.38,0,0,1-.38-0.38v-0.57a1.64,1.64,0,0,1-1,.8,3.86,3.86,0,0,1-1.46.29,3,3,0,0,1-1.27-.26,2,2,0,0,1-.86-0.71,1.77,1.77,0,0,1-.3-1,2,2,0,0,1,.78-1.64,4.62,4.62,0,0,1,2.09-.81Zm0,1.12-1.65.27a3.77,3.77,0,0,0-1.41.46,0.92,0.92,0,0,0-.5.79,0.82,0.82,0,0,0,.32.63,1.46,1.46,0,0,0,1,.28,2.34,2.34,0,0,0,1.64-.57,1.93,1.93,0,0,0,.63-1.5v-0.35Z"/><path d="M114.31,197.48a1.11,1.11,0,0,0,.93.32H116a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38H115.1a2.24,2.24,0,0,1-1.75-.62,2.65,2.65,0,0,1-.56-1.83v-3.63h-1a0.38,0.38,0,0,1-.38-0.38V192.1a0.38,0.38,0,0,1,.38-0.38h1v-2.28a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v2.28h1.78a0.38,0.38,0,0,1,.38.38v0.45a0.38,0.38,0,0,1-.38.38h-1.78v3.63A1.5,1.5,0,0,0,114.31,197.48Z"/><path d="M119.06,190.51a0.36,0.36,0,0,1-.27.11H118a0.38,0.38,0,0,1-.38-0.38v-0.8a0.38,0.38,0,0,1,.38-0.38h0.8a0.38,0.38,0,0,1,.38.38v0.8A0.36,0.36,0,0,1,119.06,190.51Zm-0.14,8.39a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38V192.1a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v6.52A0.36,0.36,0,0,1,118.92,198.89Z"/><path d="M127.46,195.38q0,0.34,0,.64a3.5,3.5,0,0,1-1,2.28,3.63,3.63,0,0,1-4.66,0,3.5,3.5,0,0,1-1-2.28c0-.1,0-0.32,0-0.64s0-.57,0-0.67a3.43,3.43,0,0,1,1-2.28,3.65,3.65,0,0,1,4.68,0,3.43,3.43,0,0,1,1,2.28C127.45,194.91,127.46,195.14,127.46,195.38Zm-1.9-2a2.2,2.2,0,0,0-2.9,0,2.36,2.36,0,0,0-.6,1.44c0,0.11,0,.31,0,0.6s0,0.47,0,.57a2.36,2.36,0,0,0,.6,1.44,2.2,2.2,0,0,0,2.9,0,2.36,2.36,0,0,0,.6-1.44q0-.31,0-0.57c0-.18,0-0.38,0-0.6A2.36,2.36,0,0,0,125.56,193.34Z"/><path d="M135.57,198.89a0.36,0.36,0,0,1-.27.11h-0.52a0.38,0.38,0,0,1-.38-0.38v-3.7a2.23,2.23,0,0,0-.52-1.6,2.17,2.17,0,0,0-2.88,0,2.21,2.21,0,0,0-.52,1.6v3.7a0.38,0.38,0,0,1-.38.38h-0.52a0.38,0.38,0,0,1-.38-0.38V192.1a0.38,0.38,0,0,1,.38-0.38h0.52a0.38,0.38,0,0,1,.38.38v0.43a2.41,2.41,0,0,1,.85-0.68,2.91,2.91,0,0,1,1.32-.27,3.2,3.2,0,0,1,1.57.38,2.67,2.67,0,0,1,1.07,1.08,3.45,3.45,0,0,1,.39,1.67v3.91A0.36,0.36,0,0,1,135.57,198.89Z"/><path d="M137.42,193.69a1.89,1.89,0,0,1,.31-1,2.14,2.14,0,0,1,.92-0.76,3.48,3.48,0,0,1,1.49-.29,4,4,0,0,1,1.5.25,2.46,2.46,0,0,1,1,.63,1.13,1.13,0,0,1,.33.73,0.32,0.32,0,0,1-.11.24,0.38,0.38,0,0,1-.27.11h-0.46a0.51,0.51,0,0,1-.42-0.2,1.82,1.82,0,0,0-1.53-.56,2,2,0,0,0-1,.24,0.73,0.73,0,0,0-.4.66,0.69,0.69,0,0,0,.2.53,1.43,1.43,0,0,0,.59.29q0.39,0.11,1.39.34a3.37,3.37,0,0,1,1.75.81,1.86,1.86,0,0,1,.52,1.33,1.76,1.76,0,0,1-.36,1.06,2.5,2.5,0,0,1-1,.79,3.71,3.71,0,0,1-1.55.3,4,4,0,0,1-1.53-.27,2.86,2.86,0,0,1-1-.67,1.13,1.13,0,0,1-.36-0.74,0.33,0.33,0,0,1,.11-0.25,0.37,0.37,0,0,1,.27-0.1h0.5a0.83,0.83,0,0,1,.22,0,0.35,0.35,0,0,1,.18.15,1.88,1.88,0,0,0,1.64.64,2.37,2.37,0,0,0,1.16-.27,0.79,0.79,0,0,0,.5-0.68,0.66,0.66,0,0,0-.25-0.54,2,2,0,0,0-.71-0.34q-0.46-.13-1.44-0.34Q137.42,195.34,137.42,193.69Z"/></g></svg>
\ No newline at end of file
diff --git a/core/tests/build_sources/assets/constants.js b/core/tests/build_sources/assets/constants.js
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/assets/constants.js
@@ -0,0 +1,504 @@
+/* eslint-disable */
+/* Don't modify anything outside the {} brackets.
+ * Insides of the {} brackets should be formatted as a JSON object.
+ * JSON rules:
+ * 1. All keys and string values must be enclosed in double quotes.
+ * 2. Each key/value pair should be on a new line.
+ * 3. All values and keys must be constant, you can't use any Javascript
+ *    functions.
+ */
+var constants = {
+  "//": "Whether to allow custom event reporting to Google Analytics.",
+  "CAN_SEND_ANALYTICS_EVENTS": false,
+
+  "ALL_CATEGORIES": ["Algebra", "Algorithms", "Architecture", "Arithmetic",
+    "Art", "Astronomy", "Biology", "Business", "Calculus", "Chemistry",
+    "Combinatorics", "Computing", "Economics", "Education", "Engineering",
+    "English", "Environment", "Gaulish", "Geography", "Geometry", "Government",
+    "Graph Theory", "History", "Languages", "Latin", "Law", "Logic",
+    "Mathematics", "Medicine", "Music", "Philosophy", "Physics", "Poetry",
+    "Probability", "Programming", "Puzzles", "Reading", "Spanish", "Sport",
+    "Statistics", "Trigonometry", "Welcome"],
+  "ACTIVITY_TYPE_EXPLORATION": "exploration",
+  "ACTIVITY_TYPE_COLLECTION": "collection",
+  "DISABLED_EXPLORATION_IDS": ["5"],
+  "TESTING_CONSTANT": "test",
+  "LIBRARY_TILE_WIDTH_PX": 208,
+  "DASHBOARD_TYPE_CREATOR": "creator",
+  "DASHBOARD_TYPE_LEARNER": "learner",
+  "DEFAULT_COLOR": "#a33f40",
+  "DEFAULT_THUMBNAIL_ICON": "Lightbulb",
+  "DEFAULT_CATEGORY_ICON": "Lightbulb",
+
+  "//": "The default language code for an exploration.",
+  "DEFAULT_LANGUAGE_CODE": "en",
+
+  "//": [
+    "List of supported default categories. For now, each category has a specific ",
+    "color associated with it. Each category also has a thumbnail icon whose ",
+    "filename is '{{CategoryName}}.svg'."
+  ],
+  "CATEGORIES_TO_COLORS": {
+    "Mathematics": "#cd672b",
+    "Algebra": "#cd672b",
+    "Arithmetic": "#d68453",
+    "Calculus": "#b86330",
+    "Logic": "#d68453",
+    "Combinatorics": "#cf5935",
+    "Graph Theory": "#cf5935",
+    "Probability": "#cf5935",
+    "Statistics": "#cd672b",
+    "Geometry": "#d46949",
+    "Trigonometry": "#d46949",
+
+    "Algorithms": "#d0982a",
+    "Computing": "#bb8b2f",
+    "Programming": "#d9aa53",
+
+    "Astronomy": "#879d6c",
+    "Biology": "#97a766",
+    "Chemistry": "#aab883",
+    "Engineering": "#8b9862",
+    "Environment": "#aba86d",
+    "Medicine": "#97a766",
+    "Physics": "#879d6c",
+
+    "Architecture": "#6e3466",
+    "Art": "#895a83",
+    "Music": "#6a3862",
+    "Philosophy": "#613968",
+    "Poetry": "#7f507f",
+
+    "English": "#193a69",
+    "Languages": "#1b4174",
+    "Latin": "#3d5a89",
+    "Reading": "#193a69",
+    "Spanish": "#405185",
+    "Gaulish": "#1b4174",
+
+    "Business": "#387163",
+    "Economics": "#5d8b7f",
+    "Geography": "#3c6d62",
+    "Government": "#538270",
+    "History": "#3d6b52",
+    "Law": "#538270",
+
+    "Education": "#942e20",
+    "Puzzles": "#a8554a",
+    "Sport": "#893327",
+    "Welcome": "#992a2b"
+  },
+
+  "//": [
+    "List of supported language codes. Each description has a ",
+    "parenthetical part that may be stripped out to give a shorter ",
+    "description."
+  ],
+  "ALL_LANGUAGE_CODES": [{
+    "code": "en",
+    "description": "English"
+  }, {
+    "code": "ar",
+    "description": "العربية (Arabic)"
+  }, {
+    "code": "bg",
+    "description": "български (Bulgarian)"
+  }, {
+    "code": "bn",
+    "description": "বাংলা (Bangla)"
+  }, {
+    "code": "ca",
+    "description": "català (Catalan)"
+  }, {
+    "code": "zh",
+    "description": "中文 (Chinese)"
+  }, {
+    "code": "hr",
+    "description": "hrvatski (Croatian)"
+  }, {
+    "code": "cs",
+    "description": "čeština (Czech)"
+  }, {
+    "code": "da",
+    "description": "dansk (Danish)"
+  }, {
+    "code": "nl",
+    "description": "Nederlands (Dutch)"
+  }, {
+    "code": "tl",
+    "description": "Filipino (Filipino)"
+  }, {
+    "code": "fi",
+    "description": "suomi (Finnish)"
+  }, {
+    "code": "fr",
+    "description": "français (French)"
+  }, {
+    "code": "de",
+    "description": "Deutsch (German)"
+  }, {
+    "code": "el",
+    "description": "ελληνικά (Greek)"
+  }, {
+    "code": "he",
+    "description": "עברית (Hebrew)"
+  }, {
+    "code": "hi",
+    "description": "हिन्दी (Hindi)"
+  }, {
+    "code": "hu",
+    "description": "magyar (Hungarian)"
+  }, {
+    "code": "id",
+    "description": "Bahasa Indonesia (Indonesian)"
+  }, {
+    "code": "it",
+    "description": "italiano (Italian)"
+  }, {
+    "code": "ja",
+    "description": "日本語 (Japanese)"
+  }, {
+    "code": "kab",
+    "description": "Taqbaylit (Kabyle)"
+  }, {
+    "code": "ko",
+    "description": "한국어 (Korean)"
+  }, {
+    "code": "lv",
+    "description": "latviešu (Latvian)"
+  }, {
+    "code": "lt",
+    "description": "lietuvių (Lithuanian)"
+  }, {
+    "code": "no",
+    "description": "Norsk (Norwegian)"
+  }, {
+    "code": "fa",
+    "description": "فارسی (Persian)"
+  }, {
+    "code": "pl",
+    "description": "polski (Polish)"
+  }, {
+    "code": "pt",
+    "description": "português (Portuguese)"
+  }, {
+    "code": "ro",
+    "description": "română (Romanian)"
+  }, {
+    "code": "ru",
+    "description": "русский (Russian)"
+  }, {
+    "code": "sr",
+    "description": "српски (Serbian)"
+  }, {
+    "code": "sk",
+    "description": "slovenčina (Slovak)"
+  }, {
+    "code": "sl",
+    "description": "slovenščina (Slovenian)"
+  }, {
+    "code": "es",
+    "description": "español (Spanish)"
+  }, {
+    "code": "sv",
+    "description": "svenska (Swedish)"
+  }, {
+    "code": "th",
+    "description": "ภาษาไทย (Thai)"
+  }, {
+    "code": "tr",
+    "description": "Türkçe (Turkish)"
+  }, {
+    "code": "uk",
+    "description": "українська (Ukrainian)"
+  }, {
+    "code": "vi",
+    "description": "Tiếng Việt (Vietnamese)"
+  }],
+
+  "//": [
+    "NOTE TO DEVELOPERS: While adding another language, please ensure that the ",
+    "languages are in alphabetical order."
+  ],
+  "SUPPORTED_SITE_LANGUAGES": [{
+    "id": "id",
+    "text": "Bahasa Indonesia"
+  }, {
+    "id": "en",
+    "text": "English"
+  }, {
+    "id": "es",
+    "text": "Español"
+  }, {
+    "id": "pt-br",
+    "text": "Português (Brasil)"
+  }, {
+    "id": "ar",
+    "text": "العربية"
+  }, {
+    "id": "kab",
+    "text": "Taqbaylit"
+  }, {
+    "id": "vi",
+    "text": "Tiếng Việt"
+  }, {
+    "id": "hi",
+    "text": "हिन्दी"
+  }, {
+    "id": "bn",
+    "text": "বাংলা"
+  }, {
+    "id": "zh-hans",
+    "text": "中文(简体)"
+  }, {
+    "id": "zh-hant",
+    "text": "中文(繁體)"
+  }],
+
+  "//": [
+    "Related languages are used to prioritize an exploration's language when ",
+    "setting the default audio language."
+  ],
+  "SUPPORTED_AUDIO_LANGUAGES": [{
+    "id": "en",
+    "description": "English",
+    "related_languages": ["en"]
+  }, {
+    "id": "ar",
+    "description": "Arabic",
+    "related_languages": ["ar"]
+  }, {
+    "id": "bg",
+    "description": "Bulgarian",
+    "related_languages": ["bg"]
+  }, {
+    "id": "bn",
+    "description": "Bangla",
+    "related_languages": ["bn"]
+  }, {
+    "id": "ca",
+    "description": "Catalan",
+    "related_languages": ["ca"]
+  }, {
+    "id": "zh",
+    "description": "Chinese",
+    "related_languages": ["zh"]
+  }, {
+    "id": "hr",
+    "description": "Croatian",
+    "related_languages": ["hr"]
+  }, {
+    "id": "cs",
+    "description": "Czech",
+    "related_languages": ["cs"]
+  }, {
+    "id": "da",
+    "description": "Danish",
+    "related_languages": ["da"]
+  }, {
+    "id": "nl",
+    "description": "Dutch",
+    "related_languages": ["nl"]
+  }, {
+    "id": "tl",
+    "description": "Filipino",
+    "related_languages": ["tl"]
+  }, {
+    "id": "fi",
+    "description": "Finnish",
+    "related_languages": ["fi"]
+  }, {
+    "id": "fr",
+    "description": "French",
+    "related_languages": ["fr"]
+  }, {
+    "id": "de",
+    "description": "German",
+    "related_languages": ["de"]
+  }, {
+    "id": "el",
+    "description": "Greek",
+    "related_languages": ["el"]
+  }, {
+    "id": "he",
+    "description": "Hebrew",
+    "related_languages": ["he"]
+  }, {
+    "id": "hi",
+    "description": "Hindi",
+    "related_languages": ["hi"]
+  }, {
+    "id": "hi-en",
+    "description": "Hinglish",
+    "related_languages": ["hi", "en"]
+  }, {
+    "id": "hu",
+    "description": "Hungarian",
+    "related_languages": ["hu"]
+  }, {
+    "id": "id",
+    "description": "Indonesian",
+    "related_languages": ["id"]
+  }, {
+    "id": "it",
+    "description": "Italian",
+    "related_languages": ["it"]
+  }, {
+    "id": "ja",
+    "description": "Japanese",
+    "related_languages": ["ja"]
+  }, {
+    "id": "kab",
+    "description": "Kabyle",
+    "related_languages": ["kab"]
+  }, {
+    "id": "ko",
+    "description": "Korean",
+    "related_languages": ["ko"]
+  }, {
+    "id": "lv",
+    "description": "Latvian",
+    "related_languages": ["lv"]
+  }, {
+    "id": "lt",
+    "description": "Lithuanian",
+    "related_languages": ["lt"]
+  }, {
+    "id": "no",
+    "description": "Norwegian",
+    "related_languages": ["no"]
+  }, {
+    "id": "fa",
+    "description": "Persian",
+    "related_languages": ["fa"]
+  }, {
+    "id": "pl",
+    "description": "Polish",
+    "related_languages": ["pl"]
+  }, {
+    "id": "pt",
+    "description": "Portuguese",
+    "related_languages": ["pt"]
+  }, {
+    "id": "ro",
+    "description": "Romanian",
+    "related_languages": ["ro"]
+  }, {
+    "id": "ru",
+    "description": "Russian",
+    "related_languages": ["ru"]
+  }, {
+    "id": "sr",
+    "description": "Serbian",
+    "related_languages": ["sr"]
+  }, {
+    "id": "sk",
+    "description": "Slovak",
+    "related_languages": ["sk"]
+  }, {
+    "id": "sl",
+    "description": "Slovenian",
+    "related_languages": ["sl"]
+  }, {
+    "id": "es",
+    "description": "Spanish",
+    "related_languages": ["es"]
+  }, {
+    "id": "sv",
+    "description": "Swedish",
+    "related_languages": ["sw"]
+  }, {
+    "id": "th",
+    "description": "Thai",
+    "related_languages": ["th"]
+  }, {
+    "id": "tr",
+    "description": "Turkish",
+    "related_languages": ["tr"]
+  }, {
+    "id": "uk",
+    "description": "Ukrainian",
+    "related_languages": ["uk"]
+  }, {
+    "id": "vi",
+    "description": "Vietnamese",
+    "related_languages": ["vi"]
+  }],
+
+  "AUTOGENERATED_AUDIO_LANGUAGES": [{
+    "id": "en-auto",
+    "description": "English (auto)",
+    "exploration_language": "en",
+    "speech_synthesis_code": "en-GB",
+    "speech_synthesis_code_mobile": "en_US"
+  }],
+
+  "//" : "Types of view in creator dashboard page.",
+  "ALLOWED_CREATOR_DASHBOARD_DISPLAY_PREFS": {
+    "CARD": "card",
+    "LIST": "list"
+  },
+
+  "WHITELISTED_COLLECTION_IDS_FOR_SAVING_GUEST_PROGRESS": [],
+
+  "FEEDBACK_SUBJECT_MAX_CHAR_LIMIT": 50,
+
+  "ACTIVITY_STATUS_PRIVATE": "private",
+  "ACTIVITY_STATUS_PUBLIC": "public",
+
+  "SYSTEM_USER_IDS": ["admin", "OppiaMigrationBot"],
+
+  "//": [
+    "A string containing the disallowed characters in state or exploration",
+    "names. The underscore is needed because spaces in names must be converted",
+    "to underscores when displayed as part of a URL or key. The other",
+    "conventions here are derived from the Wikipedia guidelines for naming",
+    "articles."
+  ],
+  "INVALID_NAME_CHARS": [
+    ":", "#", "/", "|", "_", "%", "<", ">", "[", "]", "{", "}", "\\ufffd",
+    "\\\\", "\\u007f", "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004",
+    "\\u0005", "\\u0006", "\\u0007", "\\b", "\\t", "\\n", "\\u000b", "\\f",
+    "\\r", "\\u000e", "\\u000f", "\\u0010", "\\u0011", "\\u0012", "\\u0013",
+    "\\u0014", "\\u0015", "\\u0016", "\\u0017", "\\u0018", "\\u0019", "\\u001a",
+    "\\u001b", "\\u001c", "\\u001d", "\\u001e", "\\u001f"
+  ],
+
+  "ENABLE_GENERALIZED_FEEDBACK_THREADS": false,
+
+  "ENABLE_NEW_STRUCTURES": false,
+
+  "NEW_STATE_TEMPLATE": {
+      "classifier_model_id": null,
+      "content": {
+          "html": "",
+          "content_id": "content"
+      },
+      "interaction": {
+            "id": null,
+            "customization_args": {},
+            "answer_groups": [],
+            "default_outcome": {
+                "dest": "Introduction",
+                "feedback": {
+                    "content_id": "default_outcome",
+                    "html": ""
+                },
+                "labelled_as_correct": false,
+                "param_changes": [],
+                "refresher_exploration_id": null,
+                "missing_prerequisite_skill_id": null
+            },
+            "confirmed_unclassified_answers": [],
+            "hints": [],
+            "solution": null
+        },
+      "param_changes": [],
+      "content_ids_to_audio_translations": {
+          "content": {},
+          "default_outcome": {}
+      }
+  },
+
+  "CURRENT_STATES_SCHEMA_VERSION": 25
+};
diff --git a/core/tests/build_sources/assets/embedding_tests_dev_0.0.0.html b/core/tests/build_sources/assets/embedding_tests_dev_0.0.0.html
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/assets/embedding_tests_dev_0.0.0.html
@@ -0,0 +1,18 @@
+<h2>v0.0.0</h2>
+<p>For instructions: see the README file in /static/dev/scripts.</p>
+
+<h3>Standard embedding of the latest version</h3>
+<oppia oppia-id="0"></oppia>
+<script src="http://localhost:8181/assets/scripts/oppia-player-0.0.0.js"></script>
+
+<h3>Standard embedding of version 1 of the exploration</h3>
+<oppia oppia-id="0" exploration-version="1"></oppia>
+<script src="http://localhost:8181/assets/scripts/oppia-player-0.0.1.js"></script>
+
+<h3>ERROR: No oppia id specified</h3>
+<oppia></oppia>
+<script src="http://localhost:8181/assets/scripts/oppia-player-0.0.0.js"></script>
+
+<h3>ERROR: 404 error</h3>
+<oppia oppia-id="fake_id"></oppia>
+<script src="http://localhost:8181/assets/scripts/oppia-player-0.0.0.js"></script>
diff --git a/core/tests/build_sources/extensions/CodeRepl.py b/core/tests/build_sources/extensions/CodeRepl.py
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/CodeRepl.py
@@ -0,0 +1,87 @@
+# coding: utf-8
+#
+# Copyright 2014 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, softwar
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from extensions.interactions import base
+
+
+class CodeRepl(base.BaseInteraction):
+    """Interaction that allows programs to be input."""
+
+    name = 'Code Editor'
+    description = 'Allows learners to enter code and get it evaluated.'
+    display_mode = base.DISPLAY_MODE_SUPPLEMENTAL
+    is_trainable = True
+    _dependency_ids = ['skulpt', 'codemirror', 'code_repl_prediction']
+    answer_type = 'CodeEvaluation'
+    instructions = 'Type code in the editor'
+    narrow_instructions = 'Go to code editor'
+    needs_summary = True
+    can_have_solution = True
+    show_generic_submit_button = True
+
+    # Language options 'lua', 'scheme', 'coffeescript', 'javascript', and
+    # 'ruby' have been removed for possible later re-release.
+    _customization_arg_specs = [{
+        'name': 'language',
+        'description': 'Programming language',
+        'schema': {
+            'type': 'unicode',
+            'choices': [
+                'python',
+            ]
+        },
+        'default_value': 'python'
+    }, {
+        'name': 'placeholder',
+        'description': 'Initial code displayed',
+        'schema': {
+            'type': 'unicode',
+            'ui_config': {
+                'coding_mode': 'none',
+            },
+        },
+        'default_value': '# Type your code here.'
+    }, {
+        'name': 'preCode',
+        'description': 'Code to prepend to the learner\'s submission',
+        'schema': {
+            'type': 'unicode',
+            'ui_config': {
+                'coding_mode': 'none',
+            },
+        },
+        'default_value': ''
+    }, {
+        'name': 'postCode',
+        'description': 'Code to append after the learner\'s submission',
+        'schema': {
+            'type': 'unicode',
+            'ui_config': {
+                'coding_mode': 'none',
+            },
+        },
+        'default_value': ''
+    }]
+
+    _answer_visualization_specs = [{
+        'id': 'FrequencyTable',
+        'options': {
+            'column_headers': ['Answer', 'Count'],
+            'title': 'Top 10 answers',
+        },
+        'calculation_id': 'Top10AnswerFrequencies',
+        'addressed_info_is_supported': True,
+    }]
diff --git a/core/tests/build_sources/extensions/DragAndDropSortInput.py b/core/tests/build_sources/extensions/DragAndDropSortInput.py
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/DragAndDropSortInput.py
@@ -0,0 +1,66 @@
+# Copyright 2018 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, softwar
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Python configuration for DragAndDropSortInput interaction."""
+
+from extensions.interactions import base
+
+
+class DragAndDropSortInput(base.BaseInteraction):
+    """Interaction for Drag and Drop Sorting."""
+
+    name = 'Drag And Drop Sort'
+    description = 'Allows learners to drag and drop items for sorting.'
+    display_mode = base.DISPLAY_MODE_SUPPLEMENTAL
+    is_trainable = False
+    _dependency_ids = []
+    answer_type = 'ListOfSetsOfHtmlStrings'
+    instructions = 'Drag and drop items'
+    narrow_instructions = 'Drag and drop items'
+    needs_summary = True
+    can_have_solution = True
+    show_generic_submit_button = True
+
+    _customization_arg_specs = [{
+        'name': 'choices',
+        'description': 'Items for drag and drop',
+        'schema': {
+            'type': 'list',
+            'validators': [{
+                'id': 'has_length_at_least',
+                # NOTE: There is slightly stricter validation of the number of
+                # minimum choices in frontend. It should be at least 2 from the
+                # frontend perspective but we can't impose it here as min_value
+                # in the customization schema determines the number of RTEs that
+                # appear in the customization modal initially that needs to be
+                # 1. Here min_value: 2 and default_value: [''] aren't allowed as
+                # default_value needs to be at least of same length as min_value
+                # else schema tests for customization args will fail.
+                'min_value': 1
+            }],
+            'items': {
+                'type': 'html',
+                'ui_config': {
+                    'hide_complex_extensions': True,
+                    'placeholder': 'Enter an option for the learner to drag' +
+                                   ' and drop.',
+                },
+            },
+            'ui_config': {
+                'add_element_text': 'Add a new item',
+            }
+        },
+        'default_value': [''],
+    }]
+
+    _answer_visualization_specs = []
diff --git a/core/tests/build_sources/extensions/PythonProgramTokenizerSpec.js b/core/tests/build_sources/extensions/PythonProgramTokenizerSpec.js
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/PythonProgramTokenizerSpec.js
@@ -0,0 +1,55 @@
+// Copyright 2017 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Unit tests python program tokenizer.
+ */
+
+describe('Python program tokenizer', function() {
+  beforeEach(module('oppia'));
+
+  describe('Test python program tokenizer', function() {
+    var tokenizer;
+    beforeEach(inject(function($injector) {
+      tokenizer = $injector.get('PythonProgramTokenizer');
+    }));
+
+    it('should generate correct tokens for a program', function() {
+      var program = (
+        '# In Python, the code\n#\n#     for letter in [\'a\', \'b\']:\n#    ' +
+        '     print letter\n#\n# prints:\n#\n#     a\n#     b\ns = 0;\nfor ' +
+        'num in range(1000):\n  if num%7 == 0 or num%5 == 0:\n\ts +=x\n' +
+        'print s');
+
+      var expectedTokens = [
+        ['COMMENT', '# In Python, the code'], ['NL', ''], ['COMMENT', '#'],
+        ['NL', ''], ['COMMENT', '#     for letter in [\'a\', \'b\']:'],
+        ['NL', ''], ['COMMENT', '#         print letter'], ['NL', ''],
+        ['COMMENT', '#'], ['NL', ''], ['COMMENT', '# prints:'], ['NL', ''],
+        ['COMMENT', '#'], ['NL', ''], ['COMMENT', '#     a'], ['NL', ''],
+        ['COMMENT', '#     b'], ['NL', ''], ['NAME', 's'], ['OP', '='],
+        ['NUMBER', '0'], ['OP', ';'], ['NAME', 'for'], ['NAME', 'num'],
+        ['NAME', 'in'], ['NAME', 'range'], ['OP', '('], ['NUMBER', '1000'],
+        ['OP', ')'], ['OP', ':'], ['NAME', 'if'], ['NAME', 'num'], ['OP', '%'],
+        ['NUMBER', '7'], ['OP', '=='], ['NUMBER', '0'], ['NAME', 'or'],
+        ['NAME', 'num'], ['OP', '%'], ['NUMBER', '5'], ['OP', '=='],
+        ['NUMBER', '0'], ['OP', ':'], ['NAME', 's'], ['OP', '+='],
+        ['NAME', 'x'], ['NAME', 'print'], ['NAME', 's'], ['ENDMARKER', '']];
+
+      var tokens = tokenizer.generateTokens(program.split('\n'));
+      expect(tokens.length).toEqual(expectedTokens.length);
+      expect(tokens).toEqual(expectedTokens);
+    });
+  });
+});
diff --git a/core/tests/build_sources/extensions/base.py b/core/tests/build_sources/extensions/base.py
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/base.py
@@ -0,0 +1,278 @@
+# coding: utf-8
+#
+# Copyright 2014 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Base class for defining interactions.
+
+A note on terminology: state_customization_args refers to the values of
+customization args that are provided by an exploration editor. They are
+formatted as
+
+    {ca_name: {value: ca_value}}
+
+On the other hand, interaction.customization_args refers to a combination of
+the interaction customization arg spec and the value used. It is a list of
+dicts, each representing a customization arg -- viz.:
+
+    [{
+        'name': ca_name,
+        'value': ca_value,
+        'default_value': ...,
+        ...
+    }]
+"""
+
+import copy
+import json
+import os
+
+from core.domain import obj_services
+from core.domain import visualization_registry
+from extensions import domain
+from extensions.objects.models import objects
+import feconf
+import utils
+
+# Indicates that the learner view of the interaction should be displayed in the
+# context of the conversation.
+DISPLAY_MODE_INLINE = 'inline'
+# Indicates that the learner view of the interaction should be displayed as a
+# separate object from the conversation.
+DISPLAY_MODE_SUPPLEMENTAL = 'supplemental'
+
+ALLOWED_DISPLAY_MODES = [DISPLAY_MODE_SUPPLEMENTAL, DISPLAY_MODE_INLINE]
+
+
+class BaseInteraction(object):
+    """Base interaction definition class.
+
+    This class is not meant to be user-editable. The only methods on it should
+    be get()-type methods.
+
+    Note that all interactions should also include a thumbnail image of size
+    178 x 146 pixels. This image will be shown in the interaction selector.
+    """
+
+    # The human-readable name of the interaction. Overridden in subclasses.
+    name = ''
+    # A description of the interaction. Overridden in subclasses.
+    description = ''
+    # Describes how the interaction should be displayed -- either within the
+    # conversation ('inline'), or as a separate object ('supplemental'). In the
+    # latter case, the interaction instance is reused if two adjacent states
+    # have the same interaction id.
+    display_mode = ''
+    # Whether this interaction should be considered terminal, i.e. it ends
+    # the exploration. Defaults to False.
+    is_terminal = False
+    # Whether the interaction has only one possible answer.
+    is_linear = False
+    # Whether this interaction supports machine learning classification.
+    # TODO(chiangs): remove once classifier_services is generalized.
+    is_trainable = False
+    # Additional JS library dependencies that should be loaded in pages
+    # containing this interaction. These should correspond to names of files in
+    # feconf.DEPENDENCIES_TEMPLATES_DIR. Overridden in subclasses.
+    _dependency_ids = []
+    # The type of answer (as a string) accepted by this interaction, e.g.
+    # 'CodeEvaluation'. This should be None for linear and terminal
+    # interactions.
+    answer_type = None
+    # Customization arg specifications for the component, including their
+    # descriptions, schemas and default values. Overridden in subclasses.
+    _customization_arg_specs = []
+    # Specs for desired visualizations of recorded state answers. Overridden
+    # in subclasses.
+    _answer_visualization_specs = []
+    # Instructions for using this interaction, to be shown to the learner. Only
+    # relevant for supplemental interactions.
+    instructions = None
+    # Instructions for using this interaction, to be shown to the learner. Only
+    # shows up when view port is narrow. Only relevent for supplemental
+    # interactions.
+    narrow_instructions = None
+    # Whether the answer is long, and would benefit from being summarized.
+    needs_summary = False
+    # The heading for the 'default outcome' section in the editor. This should
+    # be None unless the interaction is linear and non-terminal.
+    default_outcome_heading = None
+    # Whether the solution feature supports this interaction.
+    can_have_solution = None
+    # Whether to show a Submit button in the progress navigation area. This is
+    # a generic submit button so do not use this if special interaction-specific
+    # behavior is required. The interaction JS must also handle the
+    # EVENT_PROGRESS_NAV_SUBMITTED event broadcast by this Submit button.
+    show_generic_submit_button = False
+
+    # Temporary cache for the rule definitions.
+    _cached_rules_dict = None
+
+    @property
+    def id(self):
+        return self.__class__.__name__
+
+    @property
+    def customization_arg_specs(self):
+        return [
+            domain.CustomizationArgSpec(**cas)
+            for cas in self._customization_arg_specs]
+
+    @property
+    def answer_visualization_specs(self):
+        return self._answer_visualization_specs
+
+    @property
+    def answer_visualizations(self):
+        result = []
+        for spec in self._answer_visualization_specs:
+            factory_cls = (
+                visualization_registry.Registry.get_visualization_class(
+                    spec['id']))
+            result.append(
+                factory_cls(
+                    spec['calculation_id'], spec['options'],
+                    spec['addressed_info_is_supported']))
+        return result
+
+    @property
+    def answer_calculation_ids(self):
+        visualizations = self.answer_visualizations
+        return set(
+            [visualization.calculation_id for visualization in visualizations])
+
+    @property
+    def dependency_ids(self):
+        return copy.deepcopy(self._dependency_ids)
+
+    def normalize_answer(self, answer):
+        """Normalizes a learner's input to this interaction."""
+        if self.answer_type is None:
+            return None
+        else:
+            return obj_services.Registry.get_object_class_by_type(
+                self.answer_type).normalize(answer)
+
+    @property
+    def rules_dict(self):
+        """A dict of rule names to rule properties."""
+        if self._cached_rules_dict is not None:
+            return self._cached_rules_dict
+
+        rules_index_dict = json.loads(
+            utils.get_file_contents(feconf.RULES_DESCRIPTIONS_FILE_PATH))
+        self._cached_rules_dict = rules_index_dict[self.id]
+
+        return self._cached_rules_dict
+
+    @property
+    def _rule_description_strings(self):
+        return {
+            rule_name: self.rules_dict[rule_name]['description']
+            for rule_name in self.rules_dict
+        }
+
+    @property
+    def html_body(self):
+        """The HTML code containing directives and templates for the
+        interaction. This contains everything needed to display the interaction
+        once the necessary attributes are supplied.
+
+        Each interaction has two directive/template pairs, one for the
+        interaction itself and the other for displaying the learner's response
+        in a read-only view after it has been submitted.
+        """
+        html_templates = utils.get_file_contents(os.path.join(
+            feconf.INTERACTIONS_DIR, self.id, '%s.html' % self.id))
+        return html_templates
+
+    @property
+    def validator_html(self):
+        """The HTML code containing validators for the interaction's
+        customization_args and submission handler.
+        """
+        return (
+            '<script>%s</script>\n' %
+            utils.get_file_contents(os.path.join(
+                feconf.INTERACTIONS_DIR,
+                self.id,
+                '%sValidationService.js' % self.id)))
+
+    def to_dict(self):
+        """Gets a dict representing this interaction. Only default values are
+        provided.
+        """
+        return {
+            'id': self.id,
+            'name': self.name,
+            'description': self.description,
+            'answer_type': self.answer_type,
+            'display_mode': self.display_mode,
+            'is_terminal': self.is_terminal,
+            'is_trainable': self.is_trainable,
+            'is_linear': self.is_linear,
+            'needs_summary': self.needs_summary,
+            'customization_arg_specs': [{
+                'name': ca_spec.name,
+                'description': ca_spec.description,
+                'default_value': ca_spec.default_value,
+                'schema': ca_spec.schema,
+            } for ca_spec in self.customization_arg_specs],
+            'instructions': self.instructions,
+            'narrow_instructions': self.narrow_instructions,
+            'default_outcome_heading': self.default_outcome_heading,
+            'rule_descriptions': self._rule_description_strings,
+            'can_have_solution': self.can_have_solution,
+            'show_generic_submit_button': self.show_generic_submit_button,
+        }
+
+    def get_rule_description(self, rule_name):
+        """Gets a rule description, given its name."""
+        if rule_name not in self.rules_dict:
+            raise Exception('Could not find rule with name %s' % rule_name)
+        else:
+            return self.rules_dict[rule_name]['description']
+
+    def get_rule_param_list(self, rule_name):
+        """Gets the parameter list for a given rule."""
+        description = self.get_rule_description(rule_name)
+
+        param_list = []
+        while description.find('{{') != -1:
+            opening_index = description.find('{{')
+            description = description[opening_index + 2:]
+
+            bar_index = description.find('|')
+            param_name = description[:bar_index]
+            description = description[bar_index + 1:]
+
+            closing_index = description.find('}}')
+            normalizer_string = description[:closing_index]
+            description = description[closing_index + 2:]
+
+            param_list.append(
+                (param_name, getattr(objects, normalizer_string))
+            )
+
+        return param_list
+
+    def get_rule_param_type(self, rule_name, rule_param_name):
+        """Gets the parameter type for a given rule parameter name."""
+        rule_param_list = self.get_rule_param_list(rule_name)
+
+        for param_name, param_type in rule_param_list:
+            if param_name == rule_param_name:
+                return param_type
+        raise Exception(
+            'Rule %s has no param called %s' % (rule_name, rule_param_name))
diff --git a/core/tests/build_sources/extensions/base_test.py b/core/tests/build_sources/extensions/base_test.py
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/base_test.py
@@ -0,0 +1,126 @@
+# coding: utf-8
+#
+# Copyright 2018 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Tests for the base action specification."""
+
+from core.domain import action_registry
+from core.platform import models
+from core.tests import test_utils
+
+(stats_models,) = models.Registry.import_models([models.NAMES.statistics])
+
+
+class ActionUnitTests(test_utils.GenericTestBase):
+    """Test that the default actions are valid."""
+
+    def test_action_properties_for_exp_start(self):
+        """Test the standard properties of exploration start action."""
+
+        action = action_registry.Registry.get_action_by_type(
+            stats_models.ACTION_TYPE_EXPLORATION_START)
+
+        action_dict = action.to_dict()
+        self.assertItemsEqual(action_dict.keys(), [
+            'customization_arg_specs'])
+        self.assertEqual(
+            action_dict['customization_arg_specs'], [{
+                'name': 'state_name',
+                'description': 'Initial state name',
+                'schema': {
+                    'type': 'unicode',
+                },
+                'default_value': ''
+            }])
+
+    def test_action_properties_for_answer_submit(self):
+        """Test the standard properties of answer submit action."""
+
+        action = action_registry.Registry.get_action_by_type(
+            stats_models.ACTION_TYPE_ANSWER_SUBMIT)
+
+        action_dict = action.to_dict()
+        self.assertItemsEqual(action_dict.keys(), [
+            'customization_arg_specs'])
+        self.assertEqual(
+            action_dict['customization_arg_specs'], [{
+                'name': 'state_name',
+                'description': 'State name',
+                'schema': {
+                    'type': 'unicode',
+                },
+                'default_value': ''
+            }, {
+                'name': 'dest_state_name',
+                'description': 'Destination state name',
+                'schema': {
+                    'type': 'unicode',
+                },
+                'default_value': ''
+            }, {
+                'name': 'interaction_id',
+                'description': 'ID of the interaction',
+                'schema': {
+                    'type': 'unicode',
+                },
+                'default_value': ''
+            }, {
+                'name': 'submitted_answer',
+                'description': 'Submitted answer',
+                'schema': {
+                    'type': 'unicode',
+                },
+                'default_value': ''
+            }, {
+                'name': 'feedback',
+                'description': 'Feedback for the submitted answer',
+                'schema': {
+                    'type': 'unicode',
+                },
+                'default_value': ''
+            }, {
+                'name': 'time_spent_state_in_msecs',
+                'description': 'Time spent in state in milliseconds',
+                'schema': {
+                    'type': 'int',
+                },
+                'default_value': 0
+            }])
+
+    def test_action_properties_for_exp_quit(self):
+        """Test the standard properties of exploration quit action."""
+
+        action = action_registry.Registry.get_action_by_type(
+            stats_models.ACTION_TYPE_EXPLORATION_QUIT)
+
+        action_dict = action.to_dict()
+        self.assertItemsEqual(action_dict.keys(), [
+            'customization_arg_specs'])
+        self.assertEqual(
+            action_dict['customization_arg_specs'], [{
+                'name': 'state_name',
+                'description': 'State name',
+                'schema': {
+                    'type': 'unicode',
+                },
+                'default_value': ''
+            }, {
+                'name': 'time_spent_in_state_in_msecs',
+                'description': 'Time spent in state in milliseconds',
+                'schema': {
+                    'type': 'int',
+                },
+                'default_value': 0
+            }])
diff --git a/core/tests/build_sources/extensions/code_repl_prediction.html b/core/tests/build_sources/extensions/code_repl_prediction.html
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/code_repl_prediction.html
@@ -0,0 +1,5 @@
+<script src="/extensions/interactions/CodeRepl/CodeReplPredictionService.js"></script>
+<script src="/extensions/classifiers/PythonProgramTokenizer.js"></script>
+<script src="/extensions/classifiers/SVMPredictionService.js"></script>
+<script src="/extensions/classifiers/CountVectorizerService.js"></script>
+<script src="/extensions/classifiers/WinnowingPreprocessingService.js"></script>
diff --git a/core/tests/build_sources/extensions/models_test.py b/core/tests/build_sources/extensions/models_test.py
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/models_test.py
@@ -0,0 +1,461 @@
+# coding: utf-8
+#
+# Copyright 2014 The Oppia Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS-IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Test calculations to get interaction answer views."""
+
+from core.domain import calculation_registry
+from core.domain import exp_domain
+from core.tests import test_utils
+from extensions.answer_summarizers import models as answer_models
+
+
+class BaseCalculationUnitTests(test_utils.GenericTestBase):
+    """Test cases for BaseCalculation."""
+
+    def test_requires_override_for_calculation(self):
+        with self.assertRaises(NotImplementedError):
+            answer_models.BaseCalculation().calculate_from_state_answers_dict(
+                state_answers_dict={})
+
+
+class CalculationUnitTestBase(test_utils.GenericTestBase):
+    """Utility methods for testing calculations."""
+
+    # TODO(brianrodri, msl): Only non-zero answer-counts are tested. Should look
+    # into adding coverage for answers with zero-frequencies.
+
+    def _create_answer_dict(
+            self, answer, time_spent_in_card=3.2, session_id='sid1',
+            classify_category=exp_domain.EXPLICIT_CLASSIFICATION):
+        return {
+            'answer': answer,
+            'time_spent_in_sec': time_spent_in_card,
+            'session_id': session_id,
+            'classification_categorization': classify_category,
+        }
+
+    def _create_state_answers_dict(
+            self, answer_dicts_list, exploration_id='0', exploration_version=1,
+            state_name='Welcome!', interaction_id='MultipleChoiceInput'):
+        """Builds a simple state_answers_dict with optional default values."""
+        return {
+            'exploration_id': exploration_id,
+            'exploration_version': exploration_version,
+            'state_name': state_name,
+            'interaction_id': interaction_id,
+            'submitted_answer_list': answer_dicts_list,
+        }
+
+    def _get_calculation_instance(self):
+        """Requires the existance of the class constant: CALCULATION_ID."""
+        if not hasattr(self, 'CALCULATION_ID'):
+            raise NotImplementedError(
+                'Subclasses must provide a value for CALCULATION_ID.')
+        return calculation_registry.Registry.get_calculation_by_id(
+            self.CALCULATION_ID)
+
+    def _perform_calculation(self, state_answers_dict):
+        """Performs calculation on state_answers_dict and returns its output."""
+        calculation_instance = self._get_calculation_instance()
+        state_answers_calc_output = (
+            calculation_instance.calculate_from_state_answers_dict(
+                state_answers_dict))
+        self.assertEqual(
+            state_answers_calc_output.calculation_id,
+            self.CALCULATION_ID)
+        return state_answers_calc_output.calculation_output
+
+
+class AnswerFrequenciesUnitTestCase(CalculationUnitTestBase):
+    """Tests for arbitrary answer frequency calculations."""
+
+    CALCULATION_ID = 'AnswerFrequencies'
+
+    def test_top_answers_without_ties(self):
+        # Create 12 answers with different frequencies.
+        answers = (
+            ['A'] * 12 + ['B'] * 11 + ['C'] * 10 + ['D'] * 9 +
+            ['E'] * 8 + ['F'] * 7 + ['G'] * 6 + ['H'] * 5 +
+            ['I'] * 4 + ['J'] * 3 + ['K'] * 2 + ['L'])
+        answer_dicts_list = [self._create_answer_dict(a) for a in answers]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        # All 12 should be sorted.
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 12},
+            {'answer': 'B', 'frequency': 11},
+            {'answer': 'C', 'frequency': 10},
+            {'answer': 'D', 'frequency': 9},
+            {'answer': 'E', 'frequency': 8},
+            {'answer': 'F', 'frequency': 7},
+            {'answer': 'G', 'frequency': 6},
+            {'answer': 'H', 'frequency': 5},
+            {'answer': 'I', 'frequency': 4},
+            {'answer': 'J', 'frequency': 3},
+            {'answer': 'K', 'frequency': 2},
+            {'answer': 'L', 'frequency': 1},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+    def test_answers_with_ties(self):
+        """Ties are resolved by submission ordering: earlier ranks higher."""
+        answers = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
+        answer_dicts_list = [self._create_answer_dict(a) for a in answers]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        # All 12 should appear in-order.
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 1},
+            {'answer': 'B', 'frequency': 1},
+            {'answer': 'C', 'frequency': 1},
+            {'answer': 'D', 'frequency': 1},
+            {'answer': 'E', 'frequency': 1},
+            {'answer': 'F', 'frequency': 1},
+            {'answer': 'G', 'frequency': 1},
+            {'answer': 'H', 'frequency': 1},
+            {'answer': 'I', 'frequency': 1},
+            {'answer': 'J', 'frequency': 1},
+            {'answer': 'K', 'frequency': 1},
+            {'answer': 'L', 'frequency': 1},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+
+class Top5AnswerFrequenciesUnitTestCase(CalculationUnitTestBase):
+    """Tests for Top 5 answer frequency calculations."""
+
+    CALCULATION_ID = 'Top5AnswerFrequencies'
+
+    def test_top5_without_ties(self):
+        """Simplest case: ordering is obvious."""
+        # Create 12 answers with different frequencies.
+        answers = (
+            ['A'] * 12 + ['B'] * 11 + ['C'] * 10 + ['D'] * 9 +
+            ['E'] * 8 + ['F'] * 7 + ['G'] * 6 + ['H'] * 5 +
+            ['I'] * 4 + ['J'] * 3 + ['K'] * 2 + ['L'])
+        answer_dicts_list = [self._create_answer_dict(a) for a in answers]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        # Only top 5 are kept.
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 12},
+            {'answer': 'B', 'frequency': 11},
+            {'answer': 'C', 'frequency': 10},
+            {'answer': 'D', 'frequency': 9},
+            {'answer': 'E', 'frequency': 8},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+    def test_top5_with_ties(self):
+        """Ties are resolved by submission ordering: earlier ranks higher."""
+        # Create 12 answers with same frequencies.
+        answers = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
+        answer_dicts_list = [self._create_answer_dict(a) for a in answers]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        # Only first 5 are kept.
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 1},
+            {'answer': 'B', 'frequency': 1},
+            {'answer': 'C', 'frequency': 1},
+            {'answer': 'D', 'frequency': 1},
+            {'answer': 'E', 'frequency': 1},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+
+class Top10AnswerFrequenciesUnitTestCase(CalculationUnitTestBase):
+    """Tests for Top 10 answer frequency calculations."""
+
+    CALCULATION_ID = 'Top10AnswerFrequencies'
+
+    def test_top10_answers_without_ties(self):
+        # Create 12 answers with different frequencies.
+        answers = (
+            ['A'] * 12 + ['B'] * 11 + ['C'] * 10 + ['D'] * 9 +
+            ['E'] * 8 + ['F'] * 7 + ['G'] * 6 + ['H'] * 5 +
+            ['I'] * 4 + ['J'] * 3 + ['K'] * 2 + ['L'])
+        answer_dicts_list = [self._create_answer_dict(a) for a in answers]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        # Only top 10 are kept.
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 12},
+            {'answer': 'B', 'frequency': 11},
+            {'answer': 'C', 'frequency': 10},
+            {'answer': 'D', 'frequency': 9},
+            {'answer': 'E', 'frequency': 8},
+            {'answer': 'F', 'frequency': 7},
+            {'answer': 'G', 'frequency': 6},
+            {'answer': 'H', 'frequency': 5},
+            {'answer': 'I', 'frequency': 4},
+            {'answer': 'J', 'frequency': 3},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+    def test_top10_with_ties(self):
+        """Ties are resolved by submission ordering: earlier ranks higher."""
+        # Create 12 answers with same frequencies.
+        answers = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L']
+        answer_dicts_list = [self._create_answer_dict(a) for a in answers]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        # Only first 10 are kept.
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 1},
+            {'answer': 'B', 'frequency': 1},
+            {'answer': 'C', 'frequency': 1},
+            {'answer': 'D', 'frequency': 1},
+            {'answer': 'E', 'frequency': 1},
+            {'answer': 'F', 'frequency': 1},
+            {'answer': 'G', 'frequency': 1},
+            {'answer': 'H', 'frequency': 1},
+            {'answer': 'I', 'frequency': 1},
+            {'answer': 'J', 'frequency': 1},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+
+class FrequencyCommonlySubmittedElementsUnitTestCase(CalculationUnitTestBase):
+    """This calculation only works on answers which are all lists."""
+    CALCULATION_ID = 'FrequencyCommonlySubmittedElements'
+
+    def test_shared_answers(self):
+        answer_dicts_list = [
+            self._create_answer_dict(['B', 'A']),
+            self._create_answer_dict(['A', 'C']),
+            self._create_answer_dict(['D']),
+            self._create_answer_dict(['B', 'A']),
+        ]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 3},
+            {'answer': 'B', 'frequency': 2},
+            {'answer': 'C', 'frequency': 1},
+            {'answer': 'D', 'frequency': 1},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+    def test_many_shared_answers(self):
+        answers = (
+            ['A'] * 12 + ['B'] * 11 + ['C'] * 10 + ['D'] * 9 +
+            ['E'] * 8 + ['F'] * 7 + ['G'] * 6 + ['H'] * 5 +
+            ['I'] * 4 + ['J'] * 3 + ['K'] * 2 + ['L'])
+        split_len = len(answers) // 4
+        answer_dicts_list = [
+            self._create_answer_dict(answers[:split_len * 1]),
+            self._create_answer_dict(answers[split_len * 1:split_len * 2]),
+            self._create_answer_dict(answers[split_len * 2:split_len * 3]),
+            self._create_answer_dict(answers[split_len * 3:]),
+        ]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        # Only top 10 are kept.
+        expected_calc_output = [
+            {'answer': 'A', 'frequency': 12},
+            {'answer': 'B', 'frequency': 11},
+            {'answer': 'C', 'frequency': 10},
+            {'answer': 'D', 'frequency': 9},
+            {'answer': 'E', 'frequency': 8},
+            {'answer': 'F', 'frequency': 7},
+            {'answer': 'G', 'frequency': 6},
+            {'answer': 'H', 'frequency': 5},
+            {'answer': 'I', 'frequency': 4},
+            {'answer': 'J', 'frequency': 3},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+
+class TopAnswersByCategorizationUnitTestCase(CalculationUnitTestBase):
+    CALCULATION_ID = 'TopAnswersByCategorization'
+
+    def test_empty_state_answers_dict(self):
+        state_answers_dict = self._create_state_answers_dict([])
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        expected_calc_output = {}
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+    def test_only_one_category(self):
+        answer_dicts_list = [
+            self._create_answer_dict(
+                'Hard A', classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+        ]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        expected_calc_output = {
+            'explicit': [{'answer': 'Hard A', 'frequency': 1}],
+        }
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+    def test_many_categories(self):
+        answer_dicts_list = [
+            # EXPLICIT.
+            self._create_answer_dict(
+                'Explicit A',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            self._create_answer_dict(
+                'Explicit B',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            self._create_answer_dict(
+                'Explicit A',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            # TRAINING DATA.
+            self._create_answer_dict(
+                'Trained data A',
+                classify_category=exp_domain.TRAINING_DATA_CLASSIFICATION),
+            self._create_answer_dict(
+                'Trained data B',
+                classify_category=exp_domain.TRAINING_DATA_CLASSIFICATION),
+            self._create_answer_dict(
+                'Trained data B',
+                classify_category=exp_domain.TRAINING_DATA_CLASSIFICATION),
+            # STATS CLASSIFIER.
+            self._create_answer_dict(
+                'Stats B',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            self._create_answer_dict(
+                'Stats C',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            self._create_answer_dict(
+                'Stats C',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            self._create_answer_dict(
+                'Trained data B',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            # DEFAULT OUTCOMES.
+            self._create_answer_dict(
+                'Default C',
+                classify_category=exp_domain.DEFAULT_OUTCOME_CLASSIFICATION),
+            self._create_answer_dict(
+                'Default C',
+                classify_category=exp_domain.DEFAULT_OUTCOME_CLASSIFICATION),
+            self._create_answer_dict(
+                'Default B',
+                classify_category=exp_domain.DEFAULT_OUTCOME_CLASSIFICATION),
+        ]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        expected_calc_output = {
+            'explicit': [
+                {'answer': 'Explicit A', 'frequency': 2},
+                {'answer': 'Explicit B', 'frequency': 1},
+            ],
+            'training_data_match': [
+                {'answer': 'Trained data B', 'frequency': 2},
+                {'answer': 'Trained data A', 'frequency': 1},
+            ],
+            'statistical_classifier': [
+                {'answer': 'Stats C', 'frequency': 2},
+                {'answer': 'Stats B', 'frequency': 1},
+                {'answer': 'Trained data B', 'frequency': 1},
+            ],
+            'default_outcome': [
+                {'answer': 'Default C', 'frequency': 2},
+                {'answer': 'Default B', 'frequency': 1},
+            ],
+        }
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+
+class TopNUnresolvedAnswersByFrequency(CalculationUnitTestBase):
+    CALCULATION_ID = 'TopNUnresolvedAnswersByFrequency'
+
+    def test_empty_state_answers_dict(self):
+        state_answers_dict = self._create_state_answers_dict([])
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        expected_calc_output = []
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
+
+    def test_unresolved_answers_list(self):
+        answer_dicts_list = [
+            # EXPLICIT.
+            self._create_answer_dict(
+                'Explicit A',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            self._create_answer_dict(
+                'Explicit B',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            self._create_answer_dict(
+                'Explicit A',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            # TRAINING DATA.
+            self._create_answer_dict(
+                'Trained data A',
+                classify_category=exp_domain.TRAINING_DATA_CLASSIFICATION),
+            self._create_answer_dict(
+                'Trained data B',
+                classify_category=exp_domain.TRAINING_DATA_CLASSIFICATION),
+            self._create_answer_dict(
+                'Trained data B',
+                classify_category=exp_domain.TRAINING_DATA_CLASSIFICATION),
+            # STATS CLASSIFIER.
+            self._create_answer_dict(
+                'Stats B',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            self._create_answer_dict(
+                'Stats C',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            self._create_answer_dict(
+                'Stats C',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            self._create_answer_dict(
+                'Explicit B',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+            # EXPLICIT.
+            self._create_answer_dict(
+                'Trained data B',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            # DEFAULT OUTCOMES.
+            self._create_answer_dict(
+                'Default C',
+                classify_category=exp_domain.DEFAULT_OUTCOME_CLASSIFICATION),
+            self._create_answer_dict(
+                'Default C',
+                classify_category=exp_domain.DEFAULT_OUTCOME_CLASSIFICATION),
+            self._create_answer_dict(
+                'Default B',
+                classify_category=exp_domain.DEFAULT_OUTCOME_CLASSIFICATION),
+            # EXPLICIT.
+            self._create_answer_dict(
+                'Default B',
+                classify_category=exp_domain.EXPLICIT_CLASSIFICATION),
+            # STATS CLASSIFIER.
+            self._create_answer_dict(
+                'Default B',
+                classify_category=exp_domain.STATISTICAL_CLASSIFICATION),
+        ]
+        state_answers_dict = self._create_state_answers_dict(answer_dicts_list)
+
+        actual_calc_output = self._perform_calculation(state_answers_dict)
+        expected_calc_output = [
+            {'answer': 'Default B', 'frequency': 3},
+            {'answer': 'Explicit B', 'frequency': 2},
+            {'answer': 'Stats C', 'frequency': 2},
+            {'answer': 'Default C', 'frequency': 2},
+            {'answer': 'Stats B', 'frequency': 1},
+        ]
+        self.assertEqual(actual_calc_output.to_raw_type(), expected_calc_output)
diff --git a/core/tests/build_sources/extensions/plugin.js b/core/tests/build_sources/extensions/plugin.js
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/extensions/plugin.js
@@ -0,0 +1,36 @@
+// Copyright 2018 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Adds pre plugin to CKEditor RTE.
+CKEDITOR.plugins.add('pre', {
+  icons: 'pre',
+
+  init: function(editor) {
+    var tag = 'pre';
+    var style = new CKEDITOR.style({element: 'pre'});
+
+    editor.addCommand('pre', new CKEDITOR.styleCommand(style));
+
+    // This part will provide toolbar button highlighting in editor.
+    editor.attachStyleStateChange(style, function(state) {
+      !editor.readOnly && editor.getCommand('pre').setState(state);
+    });
+
+    editor.ui.addButton( 'Pre', {
+      label: 'Pre',
+      command: 'pre',
+      toolbar: 'insert'
+    });
+  }
+});
diff --git a/core/tests/build_sources/templates/base.html b/core/tests/build_sources/templates/base.html
new file mode 100755
--- /dev/null
+++ b/core/tests/build_sources/templates/base.html
@@ -0,0 +1,232 @@
+{% macro warnings_and_loader() -%}
+  <div tabindex="0" aria-label="Oppia Main Content" id="oppia-main-content" class="protractor-test-main-content" ng-cloak>
+    <div class="oppia-toast-container toast-top-center">
+      <div ng-repeat="warning in (AlertsService.warnings | limitTo:5) track by $index" class="toast toast-warning oppia-toast">
+        <button type="button" class="toast-close-button" ng-click="AlertsService.deleteWarning(warning)" role="button">&times;</button>
+        <div class="toast-message">
+          <[warning.content]>
+        </div>
+      </div>
+    </div>
+
+    <div>
+      <div ng-repeat="message in AlertsService.messages track by $index">
+        <alert-message message-object="message" message-index="$index"></alert-message>
+      </div>
+    </div>
+
+    <div ng-show="loadingMessage" class="oppia-loading-fullpage">
+      <div class="oppia-align-center">
+        <span translate="<[loadingMessage]>"></span>
+        <span class="oppia-loading-dot-one">.</span>
+        <span class="oppia-loading-dot-two">.</span>
+        <span class="oppia-loading-dot-three">.</span>
+      </div>
+    </div>
+    <div ng-show="!loadingMessage">
+      {% block content %}
+      {% endblock %}
+      {% block footer %}
+      {% endblock %}
+    </div>
+  </div>
+{%- endmacro %}
+
+<!DOCTYPE html>
+<html ng-app="oppia" lang="<[currentLang]>" ng-controller="Base" itemscope itemtype="http://schema.org/Organization">
+  <head>
+    {% block prerender %}
+    {% endblock prerender %}
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
+
+    <!-- Tiles for Internet Explorer. -->
+    <meta name="application-name" content="<[siteName]>">
+    <meta name="msapplication-TileColor" content="#ffffff">
+    <meta name="msapplication-square70x70logo" content="{{get_complete_static_resource_url(DOMAIN_URL, '/assets/images/logo/msapplication-tiny.png')}}">
+    <meta name="msapplication-square150x150logo" content="{{get_complete_static_resource_url(DOMAIN_URL, '/assets/images/logo/msapplication-square.png')}}">
+    <meta name="msapplication-wide310x150logo" content="{{get_complete_static_resource_url(DOMAIN_URL, '/assets/images/logo/msapplication-wide.png')}}">
+    <meta name="msapplication-square310x310logo" content="{{get_complete_static_resource_url(DOMAIN_URL, '/assets/images/logo/msapplication-large.png')}}">
+
+    <!-- The itemprops are for G+ sharing. -->
+    <meta itemprop="name" content="{{meta_name}}">
+    <meta itemprop="description" content="{{meta_description}}">
+    <!-- The og tags are for Facebook sharing. -->
+    <meta property="og:title" content="{{meta_name}}">
+    <meta property="og:site_name" content="Oppia">
+    <meta property="og:url" content="{{FULL_URL}}">
+    <meta property="og:description" content="{{meta_description}}">
+    <meta property="og:type" content="article">
+    <meta property="og:image" content="{{get_complete_static_resource_url(DOMAIN_URL, '/assets/images/logo/288x288_logo_mint.png')}}">
+
+    <link rel="apple-touch-icon" href="/assets/images/logo/favicon.png">
+
+    <!-- The title is bound to the rootScope. The content of the block
+    maintitle can be a string or a translation id. If it is a translation it
+    will be replaced by its translation when the page is loading. If it is a
+    string it would be displayed as is. This is the only way to translate
+    the page title because the head of the file is outside the scope of
+    any other controller. -->
+    <title itemprop="name" translate="{% block maintitle %}Oppia{% endblock maintitle %}"></title>
+    {% block base_url %}
+    {% endblock base_url %}
+
+    {% block header_css %}
+      {% include 'pages/header_css_libs.html' %}
+    {% endblock header_css %}
+
+    <script>
+      var GLOBALS = {
+        ADDITIONAL_ANGULAR_MODULES: JSON.parse(
+          '{{additional_angular_modules|js_string}}'),
+        can_create_collections: JSON.parse(
+          '{{can_create_collections|js_string}}'),
+        csrf_token: JSON.parse('{{csrf_token|js_string}}'),
+        DEV_MODE: JSON.parse('{{DEV_MODE|js_string}}'),
+        status_code: JSON.parse('{{status_code}}'),
+        GCS_RESOURCE_BUCKET_NAME: JSON.parse('{{GCS_RESOURCE_BUCKET_NAME|js_string}}'),
+        NAV_MODE: JSON.parse('{{nav_mode|js_string}}'),
+        preferredSiteLanguageCode: JSON.parse(
+          '{{preferred_site_language_code|js_string}}'),
+        SITE_FEEDBACK_FORM_URL: JSON.parse('{{SITE_FEEDBACK_FORM_URL|js_string}}'),
+        iframed: JSON.parse('{{iframed|js_string}}'),
+        isAdmin: JSON.parse('{{is_admin|js_string}}'),
+        isModerator: JSON.parse('{{is_moderator|js_string}}'),
+        isSuperAdmin: JSON.parse('{{is_super_admin|js_string}}'),
+        loginUrl: JSON.parse('{{login_url|js_string}}'),
+        logoutUrl: JSON.parse('{{logout_url|js_string}}'),
+        userIsLoggedIn: JSON.parse('{{user_is_logged_in|js_string}}'),
+        username: JSON.parse('{{username|js_string}}'),
+        allowYamlFileUpload: JSON.parse('{{allow_yaml_file_upload|js_string}}'),
+        PROMO_BAR_IS_ENABLED: JSON.parse('{{promo_bar_enabled|js_string}}'),
+        PROMO_BAR_MESSAGE: JSON.parse('{{promo_bar_message|js_string}}')
+      };
+    </script>
+
+    {% block header_js %}
+      {% include 'pages/header_js_libs.html' %}
+    {% endblock header_js %}
+
+    {{BEFORE_END_HEAD_TAG_HOOK}}
+  </head>
+
+  <body>
+    <div ng-if="iframed">
+      {{ warnings_and_loader() }}
+    </div>
+    <div ng-if="!iframed">
+      <div role="button" tabindex="0" ng-click="skipToMainContent()" class="oppia-skip-to-content protractor-test-skip-link">Skip to Main Content</div>
+      <promo-bar ng-if="promoBarIsEnabled" promo-message="promoBarMessage">
+      </promo-bar>
+      <div ng-if="isBackgroundMaskActive()" class="ng-cloak oppia-background-mask">
+      </div>
+
+      <div class="oppia-base-container"
+           ng-class="{'oppia-sidebar-menu-open': isSidebarShown(), 'oppia-sidebar-menu-closed': !isSidebarShown()}"
+           ng-swipe-left="closeSidebarOnSwipe()" ng-swipe-disable-mouse="false">
+        <div class="oppia-content-container">
+          <div id="wrapper">
+            <div class="oppia-main-body">
+              <nav class="navbar navbar-default oppia-navbar oppia-prevent-selection" role="navigation" headroom tolerance="0" offset="0">
+                <div class="navbar-container">
+                  <top-navigation-bar></top-navigation-bar>
+                  <div class="collapse navbar-collapse oppia-navbar-collapse ng-cloak">
+                    {% block navbar_breadcrumb %}
+                    {% endblock navbar_breadcrumb %}
+
+                    {% block local_top_nav_options %}
+                    {% endblock local_top_nav_options %}
+                  </div>
+                </div>
+              </nav>
+
+              <div class="oppia-top-of-page-padding">
+              </div>
+
+              {{ warnings_and_loader() }}
+            </div>
+
+            <noscript>
+              <div class="oppia-page-cards-container">
+                <div class="md-default-theme oppia-page-card oppia-long-text">
+                  <!-- Note to developers: We replicate the translated text inline because, without JavaScript enabled, the translation engine doesn't kick in.-->
+                  <h2>
+                    <span translate="I18N_SPLASH_JAVASCRIPT_ERROR_TITLE">We Need JavaScript in Your Browser</span>
+                    <i class="material-icons">&#xE811;</i>
+                  </h2>
+                  <p translate="I18N_SPLASH_JAVASCRIPT_ERROR_DESCRIPTION"
+                     translate-values="{hrefUrl: 'http://www.enable-javascript.com/'}">
+                     Oppia is a free, open-source learning platform full of interactive activities called 'explorations'.  Sadly, Oppia requires JavaScript to be enabled in your web browser in order to function properly and your web browser has JavaScript disabled.  If you need help enabling JavaScript, <a href="http://www.enable-javascript.com">click here.</a>
+                  </p>
+                  <p translate="I18N_SPLASH_JAVASCRIPT_ERROR_THANKS">Thank you.</p>
+                </div>
+              </div>
+            </noscript>
+
+            <side-navigation-bar></side-navigation-bar>
+          </div>
+        </div>
+      </div>
+
+      <div ng-if="DEV_MODE" class="oppia-dev-mode">
+        Dev Mode
+      </div>
+
+      <a ng-if="siteFeedbackFormUrl" ng-href="<[siteFeedbackFormUrl]>" target="_blank"
+         class="oppia-site-feedback oppia-transition-200">
+        <i class="material-icons md-18">&#xE87F;</i>
+        <span translate="I18N_SPLASH_SITE_FEEDBACK"></span>
+      </a>
+    </div>
+
+    {% include 'pages/footer_js_libs.html' %}
+
+    <script src="/templates/dev/head/app.js"></script>
+    <script src="/templates/dev/head/directives.js"></script>
+    <script src="/templates/dev/head/filters.js"></script>
+    <script src="/templates/dev/head/i18n.js"></script>
+
+    <script src="/templates/dev/head/pages/Base.js"></script>
+
+    <script src="/templates/dev/head/services/AlertsService.js"></script>
+    <script src="/templates/dev/head/services/ContextService.js"></script>
+    <script src="/templates/dev/head/services/UtilsService.js"></script>
+    <script src="/templates/dev/head/services/DebouncerService.js"></script>
+    <script src="/templates/dev/head/services/DateTimeFormatService.js"></script>
+    <script src="/templates/dev/head/services/IdGenerationService.js"></script>
+    <script src="/templates/dev/head/services/ValidatorsService.js"></script>
+    <script src="/templates/dev/head/services/HtmlEscaperService.js"></script>
+    <script src="/templates/dev/head/services/TranslationFileHashLoaderService.js"></script>
+    <script src="/templates/dev/head/services/RteHelperService.js"></script>
+    <script src="/templates/dev/head/services/StateRulesStatsService.js"></script>
+    <script src="/templates/dev/head/services/ConstructTranslationIdsService.js"></script>
+    <script src="/templates/dev/head/services/UserService.js"></script>
+    <script src="/templates/dev/head/services/contextual/DeviceInfoService.js"></script>
+    <script src="/templates/dev/head/services/contextual/UrlService.js"></script>
+    <script src="/templates/dev/head/services/contextual/WindowDimensionsService.js"></script>
+    <script src="/templates/dev/head/services/stateful/BackgroundMaskService.js"></script>
+    <script src="/templates/dev/head/services/stateful/FocusManagerService.js"></script>
+
+    <script src="/templates/dev/head/components/alerts/AlertMessageDirective.js"></script>
+    <script src="/templates/dev/head/components/create_button/CreateActivityButtonDirective.js"></script>
+
+    <script src="/templates/dev/head/components/forms/ObjectEditorDirective.js"></script>
+    <script src="/templates/dev/head/components/promo/PromoBarDirective.js"></script>
+    <script src="/templates/dev/head/components/side_navigation_bar/SideNavigationBarDirective.js"></script>
+    <script src="/templates/dev/head/components/social_buttons/SocialButtonsDirective.js"></script>
+    <script src="/templates/dev/head/components/top_navigation_bar/TopNavigationBarDirective.js"></script>
+
+    <script src="/templates/dev/head/components/CollectionCreationService.js"></script>
+    <script src="/templates/dev/head/components/ExplorationCreationService.js"></script>
+    <script src="/templates/dev/head/components/TopicCreationService.js"></script>
+    <script src="/templates/dev/head/components/SkillCreationService.js"></script>
+    <script src="/templates/dev/head/components/StoryCreationService.js"></script>
+
+    <script src="/templates/dev/head/components/CkEditorRteDirective.js"></script>
+    <script src="/templates/dev/head/domain/sidebar/SidebarStatusService.js"></script>
+    <script src="/templates/dev/head/domain/utilities/UrlInterpolationService.js"></script>
+
+    {% block footer_js %}
+    {% endblock footer_js %}
+  </body>
+</html>
diff --git a/core/tests/build_sources/templates/pages/Base.js b/core/tests/build_sources/templates/pages/Base.js
new file mode 100644
--- /dev/null
+++ b/core/tests/build_sources/templates/pages/Base.js
@@ -0,0 +1,63 @@
+// Copyright 2014 The Oppia Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS-IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+/**
+ * @fileoverview Oppia's base controller.
+ */
+
+oppia.controller('Base', [
+  '$scope', '$rootScope', '$document', 'AlertsService', 'BackgroundMaskService',
+  'SidebarStatusService', 'SITE_NAME',
+  function($scope, $rootScope, $document, AlertsService, BackgroundMaskService,
+      SidebarStatusService, SITE_NAME) {
+    $scope.siteName = SITE_NAME;
+    $scope.AlertsService = AlertsService;
+    $scope.currentLang = 'en';
+    $scope.iframed = GLOBALS.iframed;
+    $scope.siteFeedbackFormUrl = GLOBALS.SITE_FEEDBACK_FORM_URL;
+    $scope.promoBarIsEnabled = GLOBALS.PROMO_BAR_IS_ENABLED;
+    $scope.promoBarMessage = GLOBALS.PROMO_BAR_MESSAGE;
+
+    $rootScope.DEV_MODE = GLOBALS.DEV_MODE;
+    // If this is nonempty, the whole page goes into 'Loading...' mode.
+    $rootScope.loadingMessage = '';
+
+    $scope.isSidebarShown = SidebarStatusService.isSidebarShown;
+    $scope.closeSidebarOnSwipe = SidebarStatusService.closeSidebar;
+
+    $scope.isBackgroundMaskActive = BackgroundMaskService.isMaskActive;
+
+    // Listener function to catch the change in language preference.
+    $rootScope.$on('$translateChangeSuccess', function(evt, response) {
+      $scope.currentLang = response.language;
+    });
+
+    // TODO(sll): use 'touchstart' for mobile.
+    $document.on('click', function() {
+      SidebarStatusService.onDocumentClick();
+      $scope.$apply();
+    });
+
+    $scope.skipToMainContent = function() {
+      var mainContentElement = document.getElementById('oppia-main-content');
+
+      if (!mainContentElement) {
+        throw Error('Variable mainContentElement is undefined.');
+      }
+      mainContentElement.tabIndex = -1;
+      mainContentElement.scrollIntoView();
+      mainContentElement.focus();
+    };
+  }
+]);
diff --git a/core/tests/karma.conf.js b/core/tests/karma.conf.js
--- a/core/tests/karma.conf.js
+++ b/core/tests/karma.conf.js
@@ -3,7 +3,7 @@ var prodEnv = (argv.prod_env === 'True');
 var generatedJs = 'third_party/generated/js/third_party.js';
 if (prodEnv) {
   generatedJs = (
-    'backend_prod_files/third_party/generated/js/third_party.min.js');
+    'third_party/generated/js/third_party.min.js');
 }
 
 module.exports = function(config) {
diff --git a/scripts/build_test.py b/scripts/build_test.py
--- a/scripts/build_test.py
+++ b/scripts/build_test.py
@@ -14,29 +14,312 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import StringIO
+import collections
+import os
+import random
+import subprocess
+import threading
+
 # pylint: disable=relative-import
 import build
 from core.tests import test_utils
 
 # pylint: enable=relative-import
 
+TEST_DIR = os.path.join('core', 'tests', 'build', '')
+TEST_SOURCE_DIR = os.path.join('core', 'tests', 'build_sources')
+
+MOCK_ASSETS_DEV_DIR = os.path.join(TEST_SOURCE_DIR, 'assets', '')
+MOCK_ASSETS_OUT_DIR = os.path.join(TEST_DIR, 'static', 'assets', '')
+
+MOCK_EXTENSIONS_DEV_DIR = os.path.join(TEST_SOURCE_DIR, 'extensions', '')
+
+MOCK_TEMPLATES_DEV_DIR = os.path.join(TEST_SOURCE_DIR, 'templates', '')
+
+INVALID_INPUT_FILEPATH = os.path.join(
+    TEST_DIR, 'invalid', 'path', 'to', 'input.js')
+INVALID_OUTPUT_FILEPATH = os.path.join(
+    TEST_DIR, 'invalid', 'path', 'to', 'output.js')
+
+EMPTY_DIR = os.path.join(TEST_DIR, 'empty', '')
+
+# Override Pylint's protected access rule due to multiple private functions in
+# the file.
+# pylint: disable=protected-access
+
 
 class BuildTests(test_utils.GenericTestBase):
     """Test the build methods."""
 
+    def tearDown(self):
+        super(BuildTests, self).tearDown()
+        build.safe_delete_directory_tree(TEST_DIR)
+        build.safe_delete_directory_tree(EMPTY_DIR)
+
+    def test_minify(self):
+        """Tests _minify with an invalid filepath."""
+        with self.assertRaises(subprocess.CalledProcessError) as called_process:
+            build._minify(INVALID_INPUT_FILEPATH, INVALID_OUTPUT_FILEPATH)
+        # returncode is the exit status of the child process.
+        self.assertEqual(called_process.exception.returncode, 1)
+
+    def test_minify_and_create_sourcemap(self):
+        """Tests _minify_and_create_sourcemap with an invalid filepath."""
+        with self.assertRaises(subprocess.CalledProcessError) as called_process:
+            build._minify_and_create_sourcemap(
+                INVALID_INPUT_FILEPATH, INVALID_OUTPUT_FILEPATH)
+        # returncode is the exit status of the child process.
+        self.assertEqual(called_process.exception.returncode, 1)
+
+    def test_ensure_files_exist(self):
+        """Test _ensure_files_exist raises exception with a non-existent
+        filepath.
+        """
+        non_existent_filepaths = [INVALID_INPUT_FILEPATH]
+        # Exception will be raised at first file determined to be non-existent.
+        with self.assertRaisesRegexp(
+            OSError, ('File %s does not exist.') % non_existent_filepaths[0]):
+            build._ensure_files_exist(non_existent_filepaths)
+
+    def test_join_files(self):
+        """Determine third_party.js contains the content of the first 10 JS
+        files in /third_party/static.
+        """
+        # Prepare a file_stream object from StringIO.
+        third_party_js_stream = StringIO.StringIO()
+        # Get all filepaths from manifest.json.
+        dependency_filepaths = build.get_dependencies_filepaths()
+        # Join and write all JS files in /third_party/static to file_stream.
+        build._join_files(dependency_filepaths['js'], third_party_js_stream)
+        counter = 0
+        # Only checking first 10 files.
+        JS_FILE_COUNT = 10
+        for js_filepath in dependency_filepaths['js']:
+            if counter == JS_FILE_COUNT:
+                break
+            with open(js_filepath, 'r') as js_file:
+                # Assert that each line is copied over to file_stream object.
+                for line in js_file:
+                    self.assertIn(line, third_party_js_stream.getvalue())
+            counter += 1
+
+    def test_generate_copy_tasks_for_fonts(self):
+        """Test _generate_copy_tasks_for_fonts ensures that the number of copy
+        tasks matches the number of font files.
+        """
+        copy_tasks = collections.deque()
+        # Get all filepaths from manifest.json.
+        dependency_filepaths = build.get_dependencies_filepaths()
+        # Setup a sandbox folder for copying fonts.
+        test_target = os.path.join('target', 'fonts', '')
+
+        self.assertEqual(len(copy_tasks), 0)
+        copy_tasks += build._generate_copy_tasks_for_fonts(
+            dependency_filepaths['fonts'], test_target)
+        # Asserting the same number of copy tasks and number of font files.
+        self.assertEqual(len(copy_tasks), len(dependency_filepaths['fonts']))
+
     def test_insert_hash(self):
-        # pylint: disable=protected-access
-        self.assertEqual(build._insert_hash('file.js', '123456'),
-                         'file.123456.js')
-        self.assertEqual(build._insert_hash('path/to/file.js', '654321'),
-                         'path/to/file.654321.js')
-        self.assertEqual(build._insert_hash('file.min.js', 'abcdef'),
-                         'file.min.abcdef.js')
-        self.assertEqual(build._insert_hash('path/to/file.min.js', 'fedcba'),
-                         'path/to/file.min.fedcba.js')
-        # pylint: enable=protected-access
+        """Test _insert_hash returns correct filenames with provided hashes.
+        """
+        self.assertEqual(
+            build._insert_hash('file.js', '123456'), 'file.123456.js')
+        self.assertEqual(
+            build._insert_hash(
+                'path/to/file.js', '654321'), 'path/to/file.654321.js')
+        self.assertEqual(
+            build._insert_hash('file.min.js', 'abcdef'), 'file.min.abcdef.js')
+        self.assertEqual(
+            build._insert_hash(
+                'path/to/file.min.js', 'fedcba'), 'path/to/file.min.fedcba.js')
+
+    def test_get_file_count(self):
+        """Test get_file_count returns the correct number of files, excluding
+        file with extensions in FILE_EXTENSIONS_TO_IGNORE and files that should
+        not be built.
+        """
+        all_inclusive_file_count = 0
+        for _, _, files in os.walk(MOCK_EXTENSIONS_DEV_DIR):
+            all_inclusive_file_count += len(files)
+        ignored_file_count = 0
+        for _, _, files in os.walk(MOCK_EXTENSIONS_DEV_DIR):
+            for filename in files:
+                if not build.should_file_be_built(filename) or any(
+                        filename.endswith(p)
+                        for p in build.FILE_EXTENSIONS_TO_IGNORE):
+                    ignored_file_count += 1
+        self.assertEqual(
+            all_inclusive_file_count - ignored_file_count,
+            build.get_file_count(MOCK_EXTENSIONS_DEV_DIR))
+
+    def test_compare_file_count(self):
+        """Test _compare_file_count raises exception when there is a
+        mismatched file count between 2 dirs.
+        """
+        build.ensure_directory_exists(EMPTY_DIR)
+        source_dir_file_count = build.get_file_count(EMPTY_DIR)
+        assert source_dir_file_count == 0
+        target_dir_file_count = build.get_file_count(MOCK_ASSETS_DEV_DIR)
+        # Ensure that ASSETS_DEV_DIR has at least 1 file.
+        assert target_dir_file_count > 0
+        with self.assertRaisesRegexp(
+            ValueError, ('%s files in first dir != %s files in second dir') %
+            (source_dir_file_count, target_dir_file_count)):
+            build._compare_file_count(EMPTY_DIR, MOCK_ASSETS_DEV_DIR)
+
+        # Reset EMPTY_DIRECTORY to clean state.
+        build.safe_delete_directory_tree(EMPTY_DIR)
+
+    def test_verify_filepath_hash(self):
+        """Test _verify_filepath_hash raises exception:
+            1) When there is an empty hash dict.
+            2) When a filename is expected to contain hash but does not.
+            3) When there is a hash in filename that cannot be found in
+                hash dict.
+        """
+        # Final filepath example: base.240933e7564bd72a4dde42ee23260c5f.html.
+        file_hashes = dict()
+        base_filename = 'base.html'
+        with self.assertRaisesRegexp(ValueError, 'Hash dict is empty'):
+            build._verify_filepath_hash(base_filename, file_hashes)
+
+        # Generate a random hash dict for base.html.
+        file_hashes = {base_filename: random.getrandbits(128)}
+        with self.assertRaisesRegexp(
+            ValueError, '%s is expected to contain MD5 hash' % base_filename):
+            build._verify_filepath_hash(base_filename, file_hashes)
+
+        bad_filepath = 'README'
+        with self.assertRaisesRegexp(
+            ValueError, 'Filepath has less than 2 partitions after splitting'):
+            build._verify_filepath_hash(bad_filepath, file_hashes)
+
+        hashed_base_filename = build._insert_hash(
+            base_filename, random.getrandbits(128))
+        with self.assertRaisesRegexp(
+            KeyError,
+            'Hash from file named %s does not match hash dict values' %
+            hashed_base_filename):
+            build._verify_filepath_hash(hashed_base_filename, file_hashes)
+
+    def test_process_html(self):
+        """Test process_html removes whitespaces and adds hash to filepaths."""
+        BASE_HTML_SOURCE_PATH = os.path.join(
+            MOCK_TEMPLATES_DEV_DIR, 'base.html')
+        BASE_JS_RELATIVE_PATH = os.path.join('pages', 'Base.js')
+        BASE_JS_SOURCE_PATH = os.path.join(
+            MOCK_TEMPLATES_DEV_DIR, BASE_JS_RELATIVE_PATH)
+
+        build._ensure_files_exist([BASE_HTML_SOURCE_PATH, BASE_JS_SOURCE_PATH])
+        # Prepare a file_stream object from StringIO.
+        minified_html_file_stream = StringIO.StringIO()
+        # Obtain actual file hashes of /templates to add hash to all filepaths
+        # within the HTML file. The end result will look like:
+        # E.g <script ... app.js></script>
+        # --> <script ... app.[hash].js></script>.
+        # Only need to hash Base.js.
+        with self.swap(build, 'FILE_EXTENSIONS_TO_IGNORE', ('.html',)):
+            file_hashes = build.get_file_hashes(MOCK_TEMPLATES_DEV_DIR)
+
+        # Assert that base.html has white spaces and has original filepaths.
+        with open(BASE_HTML_SOURCE_PATH, 'r') as source_base_file:
+            source_base_file_content = source_base_file.read()
+            self.assertRegexpMatches(
+                source_base_file_content, r'\s{2,}',
+                msg="No white spaces detected in %s unexpectedly"
+                % BASE_HTML_SOURCE_PATH)
+            # Look for templates/pages/Base.js in source_base_file_content.
+            self.assertIn(BASE_JS_RELATIVE_PATH, source_base_file_content)
+
+        # Build base.html file.
+        with open(BASE_HTML_SOURCE_PATH, 'r') as source_base_file:
+            build.process_html(
+                source_base_file, minified_html_file_stream, file_hashes)
+
+        minified_html_file_content = minified_html_file_stream.getvalue()
+        self.assertNotRegexpMatches(
+            minified_html_file_content, r'\s{2,}',
+            msg='All white spaces must be removed from %s' %
+            BASE_HTML_SOURCE_PATH)
+        # Assert that hashes are inserted into filenames in base.html.
+        # Final filepath in base.html example:
+        # /build/templates/head/pages/Base.081ce90f17ecdf07701d83cb860985c2.js.
+        final_filename = build._insert_hash(
+            BASE_JS_RELATIVE_PATH, file_hashes[BASE_JS_RELATIVE_PATH])
+        # Look for templates/pages/Base.081ce90f17ecdf07701d83cb860985c2.js in
+        # minified_html_file_content.
+        self.assertIn(final_filename, minified_html_file_content)
+
+    def test_should_file_be_built(self):
+        """Test should_file_be_built returns the correct boolean value for
+        filepath that should be built.
+        """
+        service_js_filepath = os.path.join('core', 'pages', 'AudioService.js')
+        spec_js_filepath = os.path.join('core', 'pages', 'AudioServiceSpec.js')
+        protractor_filepath = os.path.join('extensions', 'protractor.js')
+
+        python_controller_filepath = os.path.join('base.py')
+        pyc_test_filepath = os.path.join(
+            'core', 'controllers', 'base.pyc')
+        python_test_filepath = os.path.join(
+            'core', 'tests', 'base_test.py')
+
+        self.assertFalse(build.should_file_be_built(spec_js_filepath))
+        self.assertFalse(build.should_file_be_built(protractor_filepath))
+        self.assertTrue(build.should_file_be_built(service_js_filepath))
+
+        self.assertFalse(build.should_file_be_built(python_test_filepath))
+        self.assertFalse(build.should_file_be_built(pyc_test_filepath))
+        self.assertTrue(build.should_file_be_built(python_controller_filepath))
+
+        # Swapping out constants to check if the reverse is true.
+        # ALL JS files that ends with ...Service.js should not be built.
+        with self.swap(
+            build, 'JS_FILENAME_SUFFIXES_TO_IGNORE', ('Service.js',)):
+            self.assertFalse(build.should_file_be_built(service_js_filepath))
+            self.assertTrue(build.should_file_be_built(spec_js_filepath))
+
+    def test_hash_should_be_inserted(self):
+        """Test hash_should_be_inserted returns the correct boolean value
+        for filepath that should be hashed.
+        """
+        with self.swap(
+            build, 'FILEPATHS_NOT_TO_RENAME', (
+                '*.py', 'path/to/fonts/*', 'path/to/third_party.min.js.map',
+                'path/to/third_party.min.css.map')):
+            self.assertFalse(build.hash_should_be_inserted(
+                'path/to/fonts/fontawesome-webfont.svg'))
+            self.assertFalse(build.hash_should_be_inserted(
+                'path/to/third_party.min.css.map'))
+            self.assertFalse(build.hash_should_be_inserted(
+                'path/to/third_party.min.js.map'))
+            self.assertTrue(build.hash_should_be_inserted(
+                'path/to/wrongFonts/fonta.eot'))
+            self.assertTrue(build.hash_should_be_inserted(
+                'rich_text_components/Video/protractor.js'))
+            self.assertFalse(build.hash_should_be_inserted(
+                'main.py'))
+            self.assertFalse(build.hash_should_be_inserted(
+                'extensions/domain.py'))
+
+    def test_generate_copy_tasks_to_copy_from_source_to_target(self):
+        """Test generate_copy_tasks_to_copy_from_source_to_target queues up
+        the same number of copy tasks as the number of files in the directory.
+        """
+        assets_hashes = build.get_file_hashes(MOCK_ASSETS_DEV_DIR)
+        total_file_count = build.get_file_count(MOCK_ASSETS_DEV_DIR)
+        copy_tasks = collections.deque()
+
+        self.assertEqual(len(copy_tasks), 0)
+        copy_tasks += build.generate_copy_tasks_to_copy_from_source_to_target(
+            MOCK_ASSETS_DEV_DIR, MOCK_ASSETS_OUT_DIR, assets_hashes)
+        self.assertEqual(len(copy_tasks), total_file_count)
 
     def test_is_file_hash_provided_to_frontend(self):
+        """Test is_file_hash_provided_to_frontend returns the correct boolean
+        value for filepath that should be provided to frontend.
+        """
         with self.swap(
             build, 'FILEPATHS_PROVIDED_TO_FRONTEND',
             ('path/to/file.js', 'path/to/file.html', 'file.js')):
@@ -60,8 +343,54 @@ def test_is_file_hash_provided_to_frontend(self):
             self.assertFalse(
                 build.is_file_hash_provided_to_frontend('bad_end.css'))
 
+    def test_get_filepaths_by_extensions(self):
+        """Test get_filepaths_by_extensions only returns filepaths in
+        directory with given extensions.
+        """
+        filepaths = []
+        build.ensure_directory_exists(MOCK_ASSETS_DEV_DIR)
+        extensions = ('.json', '.svg',)
+
+        self.assertEqual(len(filepaths), 0)
+        filepaths = build.get_filepaths_by_extensions(
+            MOCK_ASSETS_DEV_DIR, extensions)
+        for filepath in filepaths:
+            self.assertTrue(any(filepath.endswith(p) for p in extensions))
+        file_count = 0
+        for _, _, filenames in os.walk(MOCK_ASSETS_DEV_DIR):
+            for filename in filenames:
+                if any(filename.endswith(p) for p in extensions):
+                    file_count += 1
+        self.assertEqual(len(filepaths), file_count)
+
+        filepaths = []
+        extensions = ('.pdf', '.viminfo', '.idea',)
+
+        self.assertEqual(len(filepaths), 0)
+        filepaths = build.get_filepaths_by_extensions(
+            MOCK_ASSETS_DEV_DIR, extensions)
+        self.assertEqual(len(filepaths), 0)
+
+    def test_get_file_hashes(self):
+        """Test get_file_hashes gets hashes of all files in directory,
+        excluding file with extensions in FILE_EXTENSIONS_TO_IGNORE.
+        """
+        # Prevent getting hashes of HTML files.
+        with self.swap(build, 'FILE_EXTENSIONS_TO_IGNORE', ('.html',)):
+            file_hashes = dict()
+            self.assertEqual(len(file_hashes), 0)
+            file_hashes = build.get_file_hashes(MOCK_EXTENSIONS_DEV_DIR)
+            self.assertGreater(len(file_hashes), 0)
+            # Assert that each hash's filepath exists and does not include files
+            # with extensions in FILE_EXTENSIONS_TO_IGNORE.
+            for filepath in file_hashes:
+                abs_filepath = os.path.join(MOCK_EXTENSIONS_DEV_DIR, filepath)
+                self.assertTrue(os.path.isfile(abs_filepath))
+                self.assertFalse(filepath.endswith('.html'))
+
     def test_filter_hashes(self):
-        # set constant to provide everything to frontend.
+        """Test filter_hashes filters the provided hash correctly."""
+        # Set constant to provide everything to frontend.
         with self.swap(build, 'FILEPATHS_PROVIDED_TO_FRONTEND', ('*',)):
             hashes = {'path/to/file.js': '123456',
                       'path/file.min.js': '123456'}
@@ -89,6 +418,9 @@ def test_filter_hashes(self):
             self.assertFalse(filtered_hashes.has_key('/file.html'))
 
     def test_get_hashes_json_file_contents(self):
+        """Test get_hashes_json_file_contents parses provided hash dict
+        correctly to JSON format.
+        """
         # set constant to provide everything to frontend.
         with self.swap(build, 'FILEPATHS_PROVIDED_TO_FRONTEND', ('*',)):
             hashes = {'path/file.js': '123456'}
@@ -101,3 +433,163 @@ def test_get_hashes_json_file_contents(self):
                 build.get_hashes_json_file_contents(hashes),
                 ('var hashes = JSON.parse(\'{"/file.min.js": "654321", '
                  '"/file.js": "123456"}\');'))
+
+    def test_execute_tasks(self):
+        """Test _execute_tasks joins all threads after executing all tasks."""
+        build_tasks = collections.deque()
+        TASK_COUNT = 2
+        count = TASK_COUNT
+        while count:
+            task = threading.Thread(
+                target=build._minify,
+                args=(INVALID_INPUT_FILEPATH, INVALID_OUTPUT_FILEPATH))
+            build_tasks.append(task)
+            count -= 1
+
+        self.assertEqual(threading.active_count(), 1)
+        build._execute_tasks(build_tasks)
+        with self.assertRaisesRegexp(
+            OSError, 'threads can only be started once'):
+            build._execute_tasks(build_tasks)
+        # Assert that all threads are joined.
+        self.assertEqual(threading.active_count(), 1)
+
+    def test_generate_build_tasks_to_build_all_files_in_directory(self):
+        """Test generate_build_tasks_to_build_all_files_in_directory queues up
+        the same number of build tasks as the number of files in the source
+        directory.
+        """
+        asset_hashes = build.get_file_hashes(MOCK_ASSETS_DEV_DIR)
+        tasks = collections.deque()
+
+        self.assertEqual(len(tasks), 0)
+        # Build all files.
+        tasks = build.generate_build_tasks_to_build_all_files_in_directory(
+            MOCK_ASSETS_DEV_DIR, MOCK_ASSETS_OUT_DIR, asset_hashes)
+        total_file_count = build.get_file_count(MOCK_ASSETS_DEV_DIR)
+        self.assertEqual(len(tasks), total_file_count)
+
+    def test_generate_build_tasks_to_build_files_from_filepaths(self):
+        """Test generate_build_tasks_to_build_files_from_filepaths queues up a
+        corresponding number of build tasks to the number of file changes.
+        """
+        new_filename = 'manifest.json'
+        recently_changed_filenames = [
+            os.path.join(MOCK_ASSETS_DEV_DIR, new_filename)]
+        asset_hashes = build.get_file_hashes(MOCK_ASSETS_DEV_DIR)
+        build_tasks = collections.deque()
+
+        self.assertEqual(len(build_tasks), 0)
+        build_tasks += build.generate_build_tasks_to_build_files_from_filepaths(
+            MOCK_ASSETS_DEV_DIR, MOCK_ASSETS_OUT_DIR,
+            recently_changed_filenames, asset_hashes)
+        self.assertEqual(len(build_tasks), len(recently_changed_filenames))
+
+        build_tasks.clear()
+        svg_filepaths = build.get_filepaths_by_extensions(
+            MOCK_ASSETS_DEV_DIR, ('.svg',))
+        # Make sure there is at least 1 SVG file.
+        self.assertGreater(len(svg_filepaths), 0)
+
+        self.assertEqual(len(build_tasks), 0)
+        build_tasks += build.generate_build_tasks_to_build_files_from_filepaths(
+            MOCK_ASSETS_DEV_DIR, MOCK_ASSETS_OUT_DIR, svg_filepaths,
+            asset_hashes)
+        self.assertEqual(len(build_tasks), len(svg_filepaths))
+
+    def test_generate_build_tasks_to_build_directory(self):
+        """Test generate_build_tasks_to_build_directory queues up a
+        corresponding number of build tasks according to the given scenario.
+        """
+        EXTENSIONS_DIRNAMES_TO_DIRPATHS = {
+            'dev_dir': MOCK_EXTENSIONS_DEV_DIR,
+            'staging_dir': os.path.join(
+                TEST_DIR, 'backend_prod_files', 'extensions', ''),
+            'out_dir': os.path.join(TEST_DIR, 'build', 'extensions', '')
+        }
+        file_hashes = build.get_file_hashes(MOCK_EXTENSIONS_DEV_DIR)
+        build_dir_tasks = collections.deque()
+        build_all_files_tasks = (
+            build.generate_build_tasks_to_build_all_files_in_directory(
+                MOCK_EXTENSIONS_DEV_DIR,
+                EXTENSIONS_DIRNAMES_TO_DIRPATHS['out_dir'],
+                file_hashes))
+        self.assertGreater(len(build_all_files_tasks), 0)
+
+        # Test for building all files when staging dir does not exist.
+        self.assertEqual(len(build_dir_tasks), 0)
+        build_dir_tasks += build.generate_build_tasks_to_build_directory(
+            EXTENSIONS_DIRNAMES_TO_DIRPATHS, file_hashes)
+        self.assertEqual(len(build_dir_tasks), len(build_all_files_tasks))
+
+        build.safe_delete_directory_tree(TEST_DIR)
+        build_dir_tasks.clear()
+
+        # Test for building only new files when staging dir exists.
+        build.ensure_directory_exists(
+            EXTENSIONS_DIRNAMES_TO_DIRPATHS['staging_dir'])
+        self.assertEqual(len(build_dir_tasks), 0)
+        build_dir_tasks += build.generate_build_tasks_to_build_directory(
+            EXTENSIONS_DIRNAMES_TO_DIRPATHS, file_hashes)
+        self.assertEqual(len(build_dir_tasks), len(build_all_files_tasks))
+
+        build.safe_delete_directory_tree(TEST_DIR)
+
+        # Build all files and save to final directory.
+        build.ensure_directory_exists(
+            EXTENSIONS_DIRNAMES_TO_DIRPATHS['staging_dir'])
+        build._execute_tasks(build_dir_tasks)
+        self.assertEqual(threading.active_count(), 1)
+        build._execute_tasks(
+            build.generate_copy_tasks_to_copy_from_source_to_target(
+                EXTENSIONS_DIRNAMES_TO_DIRPATHS['staging_dir'],
+                EXTENSIONS_DIRNAMES_TO_DIRPATHS['out_dir'], file_hashes))
+
+        build_dir_tasks.clear()
+
+        # Test for only building files that need to be rebuilt.
+        self.assertEqual(len(build_dir_tasks), 0)
+        build_dir_tasks += build.generate_build_tasks_to_build_directory(
+            EXTENSIONS_DIRNAMES_TO_DIRPATHS, build_dir_tasks)
+        file_extensions_to_always_rebuild = ('.html', '.py',)
+        always_rebuilt_filepaths = build.get_filepaths_by_extensions(
+            MOCK_EXTENSIONS_DEV_DIR, file_extensions_to_always_rebuild)
+        self.assertGreater(len(always_rebuilt_filepaths), 0)
+        self.assertEqual(len(build_dir_tasks), len(always_rebuilt_filepaths))
+
+        build.safe_delete_directory_tree(TEST_DIR)
+
+    def test_get_recently_changed_filenames(self):
+        """Test get_recently_changed_filenames detects file recently added."""
+        # Create an empty folder.
+        build.ensure_directory_exists(EMPTY_DIR)
+        # Get hashes from ASSETS_DEV_DIR to simulate a folder with built files.
+        assets_hashes = build.get_file_hashes(MOCK_ASSETS_DEV_DIR)
+        recently_changed_filenames = []
+
+        self.assertEqual(len(recently_changed_filenames), 0)
+        recently_changed_filenames = build.get_recently_changed_filenames(
+            assets_hashes, EMPTY_DIR)
+        # Since all HTML and Python files are already built, they are ignored.
+        with self.swap(build, 'FILE_EXTENSIONS_TO_IGNORE', ('.html', '.py',)):
+            self.assertEqual(
+                len(recently_changed_filenames), build.get_file_count(
+                    MOCK_ASSETS_DEV_DIR))
+
+        build.safe_delete_directory_tree(EMPTY_DIR)
+
+    def test_generate_delete_tasks_to_remove_deleted_files(self):
+        """Test generate_delete_tasks_to_remove_deleted_files queues up the
+        same number of deletion task as the number of deleted files.
+        """
+        delete_tasks = collections.deque()
+        # The empty dict means that all files should be removed.
+        file_hashes = dict()
+
+        self.assertEqual(len(delete_tasks), 0)
+        delete_tasks += build.generate_delete_tasks_to_remove_deleted_files(
+            file_hashes, MOCK_TEMPLATES_DEV_DIR)
+        self.assertEqual(
+            len(delete_tasks), build.get_file_count(MOCK_TEMPLATES_DEV_DIR))
+
+# pylint: enable=protected-access