diff --git a/scripts/api_diff_report/api_diff_report.py b/scripts/api_diff_report/api_diff_report.py
new file mode 100644
index 0000000000..860d926b1d
--- /dev/null
+++ b/scripts/api_diff_report/api_diff_report.py
@@ -0,0 +1,248 @@
+# Copyright 2023 Google LLC
+#
+# 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.
+
+import json
+import argparse
+import logging
+import os
+import api_info
+import datetime
+import pytz
+
+STATUS_ADD = 'ADDED'
+STATUS_REMOVED = 'REMOVED'
+STATUS_MODIFIED = 'MODIFIED'
+STATUS_ERROR = 'BUILD ERROR'
+
+
+def main():
+ logging.getLogger().setLevel(logging.INFO)
+
+ args = parse_cmdline_args()
+
+ pr_branch = os.path.expanduser(args.pr_branch)
+ base_branch = os.path.expanduser(args.base_branch)
+ new_api_json = json.load(
+ open(os.path.join(pr_branch, api_info.API_INFO_FILE_NAME)))
+ old_api_json = json.load(
+ open(os.path.join(base_branch, api_info.API_INFO_FILE_NAME)))
+
+ diff = generate_diff_json(new_api_json, old_api_json)
+ if diff:
+ logging.info(f'json diff: \n{json.dumps(diff, indent=2)}')
+ logging.info(f'plain text diff report: \n{generate_text_report(diff)}')
+ logging.info(f'markdown diff report: \n{generate_markdown_report(diff)}')
+ else:
+ logging.info('No API Diff Detected.')
+
+
+def generate_diff_json(new_api, old_api, level='module'):
+ """diff_json only contains module & api that has a change.
+
+ format:
+ {
+ $(module_name_1): {
+ "api_types": {
+ $(api_type_1): {
+ "apis": {
+ $(api_1): {
+ "declaration": [
+ $(api_1_declaration)
+ ],
+ "sub_apis": {
+ $(sub_api_1): {
+ "declaration": [
+ $(sub_api_1_declaration)
+ ]
+ },
+ },
+ "status": $(diff_status)
+ }
+ }
+ }
+ }
+ }
+ }
+ """
+ NEXT_LEVEL = {'module': 'api_types', 'api_types': 'apis', 'apis': 'sub_apis'}
+ next_level = NEXT_LEVEL.get(level)
+
+ diff = {}
+ for key in set(new_api.keys()).union(old_api.keys()):
+ # Added API
+ if key not in old_api:
+ diff[key] = new_api[key]
+ diff[key]['status'] = STATUS_ADD
+ if diff[key].get('declaration'):
+ diff[key]['declaration'] = [STATUS_ADD] + diff[key]['declaration']
+ # Removed API
+ elif key not in new_api:
+ diff[key] = old_api[key]
+ diff[key]['status'] = STATUS_REMOVED
+ if diff[key].get('declaration'):
+ diff[key]['declaration'] = [STATUS_REMOVED] + diff[key]['declaration']
+ # Module Build Error. If a "module" exist but have no
+ # content (e.g. doc_path), it must have a build error.
+ elif level == 'module' and (not new_api[key]['path']
+ or not old_api[key]['path']):
+ diff[key] = {'status': STATUS_ERROR}
+ # Check diff in child level and diff in declaration
+ else:
+ child_diff = generate_diff_json(new_api[key][next_level],
+ old_api[key][next_level],
+ level=next_level) if next_level else {}
+ declaration_diff = new_api[key].get('declaration') != old_api[key].get(
+ 'declaration') if level in ['apis', 'sub_apis'] else False
+
+ # No diff
+ if not child_diff and not declaration_diff:
+ continue
+
+ diff[key] = new_api[key]
+ # Changes at child level
+ if child_diff:
+ diff[key][next_level] = child_diff
+
+ # Modified API (changes in API declaration)
+ if declaration_diff:
+ diff[key]['status'] = STATUS_MODIFIED
+ diff[key]['declaration'] = [STATUS_ADD] + \
+ new_api[key]['declaration'] + \
+ [STATUS_REMOVED] + \
+ old_api[key]['declaration']
+
+ return diff
+
+
+def generate_text_report(diff, level=0, print_key=True):
+ report = ''
+ indent_str = ' ' * level
+ for key, value in diff.items():
+ # filter out ["path", "api_type_link", "api_link", "declaration", "status"]
+ if isinstance(value, dict):
+ if key in ['api_types', 'apis', 'sub_apis']:
+ report += generate_text_report(value, level=level)
+ else:
+ status_text = f"{value.get('status', '')}:" if 'status' in value else ''
+ if status_text:
+ if print_key:
+ report += f'{indent_str}{status_text} {key}\n'
+ else:
+ report += f'{indent_str}{status_text}\n'
+ if value.get('declaration'):
+ for d in value.get('declaration'):
+ report += f'{indent_str}{d}\n'
+ else:
+ report += f'{indent_str}{key}\n'
+ report += generate_text_report(value, level=level + 1)
+
+ return report
+
+
+def generate_markdown_title(commit, run_id):
+ pst_now = datetime.datetime.utcnow().astimezone(
+ pytz.timezone('America/Los_Angeles'))
+ return (
+ '## Apple API Diff Report\n' + 'Commit: %s\n' % commit
+ + 'Last updated: %s \n' % pst_now.strftime('%a %b %e %H:%M %Z %G')
+ + '**[View workflow logs & download artifacts]'
+ + '(https://github.com/firebase/firebase-ios-sdk/actions/runs/%s)**\n\n'
+ % run_id + '-----\n')
+
+
+def generate_markdown_report(diff, level=0):
+ report = ''
+ header_str = '#' * (level + 3)
+ for key, value in diff.items():
+ if isinstance(value, dict):
+ if key in ['api_types', 'apis', 'sub_apis']:
+ report += generate_markdown_report(value, level=level)
+ else:
+ current_status = value.get('status')
+ if current_status:
+ # Module level: Always print out module name as title
+ if level == 0:
+ report += f'{header_str} {key} [{current_status}]\n'
+ if current_status != STATUS_ERROR: # ADDED,REMOVED,MODIFIED
+ report += '\n\n'
+ report += f'[{current_status}] {key}\n'
+ report += '
\n\n'
+ declarations = value.get('declaration', [])
+ sub_report = generate_text_report(value, level=1, print_key=False)
+ detail = process_declarations(current_status, declarations,
+ sub_report)
+ report += f'```diff\n{detail}\n```\n\n \n\n'
+ else: # no diff at current level
+ report += f'{header_str} {key}\n'
+ report += generate_markdown_report(value, level=level + 1)
+ # Module level: Always print out divider in the end
+ if level == 0:
+ report += '-----\n'
+
+ return report
+
+
+def process_declarations(current_status, declarations, sub_report):
+ """Diff syntax highlighting in Github Markdown."""
+ detail = ''
+ if current_status == STATUS_MODIFIED:
+ for line in (declarations + sub_report.split('\n')):
+ if STATUS_ADD in line:
+ prefix = '+ '
+ continue
+ elif STATUS_REMOVED in line:
+ prefix = '- '
+ continue
+ if line:
+ detail += f'{prefix}{line}\n'
+ else:
+ prefix = '+ ' if current_status == STATUS_ADD else '- '
+ for line in (declarations + sub_report.split('\n')):
+ if line:
+ detail += f'{prefix}{line}\n'
+
+ return categorize_declarations(detail)
+
+
+def categorize_declarations(detail):
+ """Categorize API info by Swift and Objective-C."""
+ lines = detail.split('\n')
+
+ swift_lines = [line.replace('Swift', '') for line in lines if 'Swift' in line]
+ objc_lines = [
+ line.replace('Objective-C', '') for line in lines if 'Objective-C' in line
+ ]
+
+ swift_detail = 'Swift:\n' + '\n'.join(swift_lines) if swift_lines else ''
+ objc_detail = 'Objective-C:\n' + '\n'.join(objc_lines) if objc_lines else ''
+
+ if not swift_detail and not objc_detail:
+ return detail
+ else:
+ return f'{swift_detail}\n{objc_detail}'.strip()
+
+
+def parse_cmdline_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-p', '--pr_branch')
+ parser.add_argument('-b', '--base_branch')
+ parser.add_argument('-c', '--commit')
+ parser.add_argument('-i', '--run_id')
+
+ args = parser.parse_args()
+ return args
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/api_diff_report/api_info.py b/scripts/api_diff_report/api_info.py
new file mode 100644
index 0000000000..986b08bdf2
--- /dev/null
+++ b/scripts/api_diff_report/api_info.py
@@ -0,0 +1,227 @@
+# Copyright 2023 Google LLC
+#
+# 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.
+
+import json
+import argparse
+import logging
+import os
+import subprocess
+import icore_module
+from urllib.parse import unquote
+from bs4 import BeautifulSoup
+
+API_INFO_FILE_NAME = 'api_info.json'
+
+
+def main():
+ logging.getLogger().setLevel(logging.INFO)
+
+ # Parse command-line arguments
+ args = parse_cmdline_args()
+ output_dir = os.path.expanduser(args.output_dir)
+ api_theme_dir = os.path.expanduser(args.api_theme_dir)
+ if not os.path.exists(output_dir):
+ os.makedirs(output_dir)
+
+ # Detect changed modules based on changed files
+ changed_api_files = get_api_files(args.file_list)
+ if not changed_api_files:
+ logging.info('No Changed API File Detected')
+ exit(1)
+ changed_modules = icore_module.detect_changed_modules(changed_api_files)
+ if not changed_modules:
+ logging.info('No Changed Module Detected')
+ exit(1)
+
+ # Generate API documentation and parse API declarations
+ # for each changed module
+ api_container = {}
+ for _, module in changed_modules.items():
+ api_doc_dir = os.path.join(output_dir, 'doc', module['name'])
+ build_api_doc(module, api_doc_dir, api_theme_dir)
+
+ if os.path.exists(api_doc_dir):
+ module_api_container = parse_module(api_doc_dir)
+ api_container[module['name']] = {
+ 'path': api_doc_dir,
+ 'api_types': module_api_container
+ }
+ else: # api doc fail to build.
+ api_container[module['name']] = {'path': '', 'api_types': {}}
+
+ api_info_path = os.path.join(output_dir, API_INFO_FILE_NAME)
+ logging.info(f'Writing API data to {api_info_path}')
+ with open(api_info_path, 'w') as f:
+ f.write(json.dumps(api_container, indent=2))
+
+
+def get_api_files(file_list):
+ """Filter out non api files."""
+ return [
+ f for f in file_list
+ if f.endswith('.swift') or (f.endswith('.h') and 'Public' in f)
+ ]
+
+
+def build_api_doc(module, output_dir, api_theme_dir):
+ """Use Jazzy to build API documentation for a specific module's source
+ code."""
+ if module['language'] == icore_module.SWIFT:
+ logging.info('------------')
+ cmd = f'jazzy --module {module["name"]}'\
+ + ' --swift-build-tool xcodebuild'\
+ + ' --build-tool-arguments'\
+ + f' -scheme,{module["scheme"]}'\
+ + ',-destination,generic/platform=iOS,build'\
+ + f' --output {output_dir}'\
+ + f' --theme {api_theme_dir}'
+ logging.info(cmd)
+ result = subprocess.Popen(cmd,
+ universal_newlines=True,
+ shell=True,
+ stdout=subprocess.PIPE)
+ logging.info(result.stdout.read())
+ elif module['language'] == icore_module.OBJECTIVE_C:
+ logging.info('------------')
+ cmd = 'jazzy --objc'\
+ + f' --framework-root {module["root_dir"]}'\
+ + f' --umbrella-header {module["umbrella_header"]}'\
+ + f' --output {output_dir}'\
+ + f' --theme {api_theme_dir}'
+ logging.info(cmd)
+ result = subprocess.Popen(cmd,
+ universal_newlines=True,
+ shell=True,
+ stdout=subprocess.PIPE)
+ logging.info(result.stdout.read())
+
+
+def parse_module(api_doc_path):
+ """Parse "${module}/index.html" and extract necessary information
+ e.g.
+ {
+ $(api_type_1): {
+ "api_type_link": $(api_type_link),
+ "apis": {
+ $(api_name_1): {
+ "api_link": $(api_link_1),
+ "declaration": [$(swift_declaration), $(objc_declaration)],
+ "sub_apis": {
+ $(sub_api_name_1): {"declaration": [$(swift_declaration)]},
+ $(sub_api_name_2): {"declaration": [$(objc_declaration)]},
+ ...
+ }
+ },
+ $(api_name_2): {
+ ...
+ },
+ }
+ },
+ $(api_type_2): {
+ ..
+ },
+ }
+ """
+ module_api_container = {}
+ # Read the HTML content from the file
+ index_link = f'{api_doc_path}/index.html'
+ with open(index_link, 'r') as file:
+ html_content = file.read()
+
+ # Parse the HTML content
+ soup = BeautifulSoup(html_content, 'html.parser')
+
+ # Locate the element with class="nav-groups"
+ nav_groups_element = soup.find('ul', class_='nav-groups')
+ # Extract data and convert to JSON format
+ for nav_group in nav_groups_element.find_all('li', class_='nav-group-name'):
+ api_type = nav_group.find('a').text
+ api_type_link = nav_group.find('a')['href']
+
+ apis = {}
+ for nav_group_task in nav_group.find_all('li', class_='nav-group-task'):
+ api_name = nav_group_task.find('a').text
+ api_link = nav_group_task.find('a')['href']
+ apis[api_name] = {'api_link': api_link, 'declaration': [], 'sub_apis': {}}
+
+ module_api_container[api_type] = {
+ 'api_type_link': api_type_link,
+ 'apis': apis
+ }
+
+ parse_api(api_doc_path, module_api_container)
+
+ return module_api_container
+
+
+def parse_api(doc_path, module_api_container):
+ """Parse API html and extract necessary information.
+
+ e.g. ${module}/Classes.html
+ """
+ for api_type, api_type_abstract in module_api_container.items():
+ api_type_link = f'{doc_path}/{unquote(api_type_abstract["api_type_link"])}'
+ api_data_container = module_api_container[api_type]['apis']
+ with open(api_type_link, 'r') as file:
+ html_content = file.read()
+
+ # Parse the HTML content
+ soup = BeautifulSoup(html_content, 'html.parser')
+ for api in soup.find('div', class_='task-group').find_all('li',
+ class_='item'):
+ api_name = api.find('a', class_='token').text
+ for api_declaration in api.find_all('div', class_='language'):
+ api_declaration_text = ' '.join(api_declaration.stripped_strings)
+ api_data_container[api_name]['declaration'].append(api_declaration_text)
+
+ for api, api_abstruct in api_type_abstract['apis'].items():
+ if api_abstruct['api_link'].endswith('.html'):
+ parse_sub_api(f'{doc_path}/{unquote(api_abstruct["api_link"])}',
+ api_data_container[api]['sub_apis'])
+
+
+def parse_sub_api(api_link, sub_api_data_container):
+ """Parse SUB_API html and extract necessary information.
+
+ e.g. ${module}/Classes/${class_name}.html
+ """
+ with open(api_link, 'r') as file:
+ html_content = file.read()
+
+ # Parse the HTML content
+ soup = BeautifulSoup(html_content, 'html.parser')
+ for s_api in soup.find('div', class_='task-group').find_all('li',
+ class_='item'):
+ api_name = s_api.find('a', class_='token').text
+ sub_api_data_container[api_name] = {'declaration': []}
+ for api_declaration in s_api.find_all('div', class_='language'):
+ api_declaration_text = ' '.join(api_declaration.stripped_strings)
+ sub_api_data_container[api_name]['declaration'].append(
+ api_declaration_text)
+
+
+def parse_cmdline_args():
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-f', '--file_list', nargs='+', default=[])
+ parser.add_argument('-o', '--output_dir', default='output_dir')
+ parser.add_argument('-t',
+ '--api_theme_dir',
+ default='scripts/api_diff_report/theme')
+
+ args = parser.parse_args()
+ return args
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/api_diff_report/icore_module.py b/scripts/api_diff_report/icore_module.py
new file mode 100644
index 0000000000..47ef00bff9
--- /dev/null
+++ b/scripts/api_diff_report/icore_module.py
@@ -0,0 +1,183 @@
+# Copyright 2023 Google LLC
+#
+# 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.
+
+import os
+import logging
+import json
+import subprocess
+
+SWIFT = 'Swift'
+OBJECTIVE_C = 'Objective-C'
+
+# List of Swift and Objective-C modules
+MODULE_LIST = [
+ 'FirebaseABTesting',
+ 'FirebaseAnalytics', # Not buildable from source
+ 'FirebaseAnalyticsOnDeviceConversion', # Not buildable.
+ 'FirebaseAnalyticsSwift',
+ 'FirebaseAppCheck',
+ 'FirebaseAppDistribution',
+ 'FirebaseAuth',
+ 'FirebaseCore',
+ 'FirebaseCrashlytics',
+ 'FirebaseDatabase',
+ 'FirebaseDatabaseSwift',
+ 'FirebaseDynamicLinks',
+ 'FirebaseFirestore',
+ 'FirebaseFirestoreSwift',
+ 'FirebaseFunctions',
+ 'FirebaseInAppMessaging'
+ 'FirebaseInAppMessagingSwift',
+ 'FirebaseInstallations',
+ 'FirebaseMessaging',
+ 'FirebaseMLModelDownloader',
+ 'FirebasePerformance',
+ 'FirebaseRemoteConfig',
+ 'FirebaseRemoteConfigSwift',
+ # Not buildable. No scheme named "FirebaseSharedSwift"
+ 'FirebaseSharedSwift',
+ 'FirebaseStorage',
+ # Not buildable. NO "source_files"
+ 'GoogleAppMeasurement',
+ # Not buildable. NO "source_files"
+ 'GoogleAppMeasurementOnDeviceConversion'
+]
+
+
+def main():
+ module_info()
+
+
+def detect_changed_modules(changed_api_files):
+ """Detect changed modules based on changed API files."""
+ all_modules = module_info()
+ changed_modules = {}
+ for file_path in changed_api_files:
+ for k, v in all_modules.items():
+ if v['root_dir'] and v['root_dir'] in file_path:
+ changed_modules[k] = v
+ break
+
+ logging.info(f'changed_modules:\n{json.dumps(changed_modules, indent=4)}')
+ return changed_modules
+
+
+def module_info():
+ """retrieve module info in MODULE_LIST from `.podspecs`
+ The module info helps to build Jazzy
+ includes: module name, source_files, public_header_files,
+ language, umbrella_header, framework_root
+ """
+ module_from_podspecs = module_info_from_podspecs()
+ module_list = {}
+ for k, v in module_from_podspecs.items():
+ if k in MODULE_LIST:
+ if k not in module_list:
+ module_list[k] = v
+ module_list[k]['language'] = OBJECTIVE_C if v.get(
+ 'public_header_files') else SWIFT
+ module_list[k]['scheme'] = get_scheme(k)
+ module_list[k]['umbrella_header'] = get_umbrella_header(
+ k, v.get('public_header_files'))
+ module_list[k]['root_dir'] = get_root_dir(k, v.get('source_files'))
+
+ logging.info(f'all_module:\n{json.dumps(module_list, indent=4)}')
+ return module_list
+
+
+def get_scheme(module_name):
+ """Jazzy documentation Info SWIFT only.
+
+ Get scheme from module name in .podspecs Assume the scheme is the
+ same as the module name:
+ """
+ MODULE_SCHEME_PATCH = {
+ 'FirebaseInAppMessagingSwift': 'FirebaseInAppMessagingSwift-Beta',
+ }
+ if module_name in MODULE_SCHEME_PATCH:
+ return MODULE_SCHEME_PATCH[module_name]
+ return module_name
+
+
+def get_umbrella_header(module_name, public_header_files):
+ """Jazzy documentation Info OBJC only Get umbrella_header from
+ public_header_files in .podspecs Assume the umbrella_header is with the
+ format:
+
+ {module_name}/Sources/Public/{module_name}/{module_name}.h
+ """
+ if public_header_files:
+ if isinstance(public_header_files, list):
+ return public_header_files[0].replace('*', module_name)
+ elif isinstance(public_header_files, str):
+ return public_header_files.replace('*', module_name)
+ return ''
+
+
+def get_root_dir(module_name, source_files):
+ """Get source code root_dir from source_files in .podspecs Assume the
+ root_dir is with the format:
+
+ {module_name}/Sources or {module_name}/Source
+ """
+ MODULE_ROOT_PATCH = {
+ 'FirebaseFirestore': 'Firestore/Source',
+ 'FirebaseFirestoreSwift': 'Firestore/Swift/Source',
+ 'FirebaseCrashlytics': 'Crashlytics/Crashlytics',
+ 'FirebaseInAppMessagingSwift': 'FirebaseInAppMessaging/Swift/Source',
+ }
+ if module_name in MODULE_ROOT_PATCH:
+ return MODULE_ROOT_PATCH[module_name]
+ if source_files:
+ for source_file in source_files:
+ if f'{module_name}/Sources' in source_file:
+ return f'{module_name}/Sources'
+ if f'{module_name}/Source' in source_file:
+ return f'{module_name}/Source'
+ return ''
+
+
+def module_info_from_podspecs(root_dir=os.getcwd()):
+ result = {}
+ for filename in os.listdir(root_dir):
+ if filename.endswith('.podspec'):
+ podspec_data = parse_podspec(filename)
+ source_files = podspec_data.get('source_files')
+ if not podspec_data.get('source_files') and podspec_data.get('ios'):
+ source_files = podspec_data.get('ios').get('source_files')
+ result[podspec_data['name']] = {
+ 'name': podspec_data['name'],
+ 'source_files': source_files,
+ 'public_header_files': podspec_data.get('public_header_files')
+ }
+ return result
+
+
+def parse_podspec(podspec_file):
+ result = subprocess.run(f'pod ipc spec {podspec_file}',
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ shell=True)
+ if result.returncode != 0:
+ logging.info(f'Error: {result.stderr}')
+ return None
+
+ # Parse the JSON output
+ podspec_data = json.loads(result.stdout)
+ return podspec_data
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/api_diff_report/theme/templates/deprecation.mustache b/scripts/api_diff_report/theme/templates/deprecation.mustache
new file mode 100644
index 0000000000..cfcabac31f
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/deprecation.mustache
@@ -0,0 +1,12 @@
+{{#deprecation_message}}
+
+
Deprecated
+ {{{deprecation_message}}}
+
+{{/deprecation_message}}
+{{#unavailable_message}}
+
+
Unavailable
+ {{{unavailable_message}}}
+
+{{/unavailable_message}}
diff --git a/scripts/api_diff_report/theme/templates/doc.mustache b/scripts/api_diff_report/theme/templates/doc.mustache
new file mode 100644
index 0000000000..41337ce96d
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/doc.mustache
@@ -0,0 +1,88 @@
+
+
+
+ {{name}} {{kind}} Reference
+
+
+ {{#enable_katex}}
+
+ {{/enable_katex}}
+
+
+ {{#enable_katex}}
+
+ {{/enable_katex}}
+
+ {{{custom_head}}}
+ {{^disable_search}}
+
+
+
+ {{/disable_search}}
+
+
+ {{#dash_type}}
+
+ {{/dash_type}}
+
+ {{> header}}
+
+
+ {{module_name}} Reference
+
+ {{name}} {{kind}} Reference
+
+
+
+ {{> nav}}
+
+
+
+ {{^hide_name}}{{name}}
{{/hide_name}}
+ {{> deprecation}}
+ {{#declaration}}
+
+
+ {{#other_language_declaration}}
{{language}}
{{/other_language_declaration}}
+ {{{declaration}}}
+
+ {{#other_language_declaration}}
+
+
Swift
+ {{{other_language_declaration}}}
+
+ {{/other_language_declaration}}
+
+ {{/declaration}}
+ {{{overview}}}
+ {{#parameters.any?}}
+
+
Parameters
+
+
+ {{#parameters}}
+ {{> parameter}}
+ {{/parameters}}
+
+
+
+ {{/parameters.any?}}
+ {{#return}}
+
+
Return Value
+ {{{return}}}
+
+ {{/return}}
+ {{#source_host_item_url}}
+
+ {{/source_host_item_url}}
+
+ {{> tasks}}
+
+ {{> footer}}
+
+
+
+
diff --git a/scripts/api_diff_report/theme/templates/footer.mustache b/scripts/api_diff_report/theme/templates/footer.mustache
new file mode 100644
index 0000000000..2ff8911311
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/footer.mustache
@@ -0,0 +1,4 @@
+
diff --git a/scripts/api_diff_report/theme/templates/header.mustache b/scripts/api_diff_report/theme/templates/header.mustache
new file mode 100644
index 0000000000..ec1adf7cfd
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/header.mustache
@@ -0,0 +1,18 @@
+
diff --git a/scripts/api_diff_report/theme/templates/nav.mustache b/scripts/api_diff_report/theme/templates/nav.mustache
new file mode 100644
index 0000000000..7cb938c3d8
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/nav.mustache
@@ -0,0 +1,16 @@
+
diff --git a/scripts/api_diff_report/theme/templates/parameter.mustache b/scripts/api_diff_report/theme/templates/parameter.mustache
new file mode 100644
index 0000000000..6fa329d72e
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/parameter.mustache
@@ -0,0 +1,12 @@
+
+
+
+ {{name}}
+
+ |
+
+
+ {{{discussion}}}
+
+ |
+
diff --git a/scripts/api_diff_report/theme/templates/task.mustache b/scripts/api_diff_report/theme/templates/task.mustache
new file mode 100644
index 0000000000..1353dafd5b
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/task.mustache
@@ -0,0 +1,100 @@
+
+ {{#name}}
+
+ {{/name}}
+
+ {{#items}}
+ -
+
+
+
+
+ {{#direct_link}}
+ {{name}}
+
+ {{/direct_link}}
+ {{^direct_link}}
+ {{^usage_discouraged}}
+
{{{name_html}}}
+ {{/usage_discouraged}}
+ {{#usage_discouraged}}
+
{{{name_html}}}
+ {{/usage_discouraged}}
+
+ {{#declaration_note}}
+
+ {{.}}
+
+ {{/declaration_note}}
+
+
+
+
+
+ {{> deprecation}}
+ {{#abstract}}
+
+ {{{abstract}}}
+ {{#url}}
+
See more
+ {{/url}}
+
+ {{/abstract}}
+ {{#default_impl_abstract}}
+ Default Implementation
+
+ {{{default_impl_abstract}}}
+
+ {{/default_impl_abstract}}
+ {{#declaration}}
+
+
Declaration
+
+
{{language}}
+ {{{declaration}}}
+
+ {{#other_language_declaration}}
+
+
Swift
+ {{{other_language_declaration}}}
+
+ {{/other_language_declaration}}
+
+ {{/declaration}}
+ {{#parameters.count}}
+
+
Parameters
+
+
+ {{#parameters}}
+ {{> parameter}}
+ {{/parameters}}
+
+
+
+ {{/parameters.count}}
+ {{#return}}
+
+
Return Value
+ {{{return}}}
+
+ {{/return}}
+ {{#source_host_item_url}}
+
+ {{/source_host_item_url}}
+
+ {{/direct_link}}
+
+
+ {{/items}}
+
+
diff --git a/scripts/api_diff_report/theme/templates/tasks.mustache b/scripts/api_diff_report/theme/templates/tasks.mustache
new file mode 100644
index 0000000000..f97bb437f3
--- /dev/null
+++ b/scripts/api_diff_report/theme/templates/tasks.mustache
@@ -0,0 +1,7 @@
+{{#tasks.count}}
+
+ {{#tasks}}
+ {{> task}}
+ {{/tasks}}
+
+{{/tasks.count}}