├── .coveragerc ├── .flake8 ├── .github ├── CODEOWNERS ├── release_notes.py ├── renovate.json5 └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .prettierignore ├── .prettierrc.yaml ├── LICENSE ├── MANIFEST.in ├── README.md ├── VERSION ├── aip_site ├── __init__.py ├── cli.py ├── jinja │ ├── __init__.py │ ├── env.py │ ├── ext │ │ ├── __init__.py │ │ ├── sample.py │ │ └── tab.py │ └── loaders.py ├── md.py ├── models │ ├── __init__.py │ ├── aip.py │ ├── page.py │ ├── scope.py │ └── site.py ├── publisher.py ├── server.py ├── support │ ├── assets │ │ ├── favicon.ico │ │ ├── images │ │ │ ├── github.png │ │ │ └── glue-icons.svg │ │ ├── js │ │ │ ├── aip │ │ │ │ ├── aip-graphviz.js │ │ │ │ └── aip-index.js │ │ │ ├── global.js │ │ │ ├── graphviz │ │ │ │ ├── lite.render.js │ │ │ │ └── viz.js │ │ │ ├── search │ │ │ │ ├── pagination.js │ │ │ │ ├── tipuesearch.min.js │ │ │ │ └── tipuesearch_set.js │ │ │ └── syntax.js │ │ └── misc │ │ │ └── ebnf-filtering.txt │ ├── scss │ │ ├── hero.scss │ │ ├── imports │ │ │ ├── aip │ │ │ │ ├── badges.scss │ │ │ │ ├── breadcrumbs.scss │ │ │ │ ├── header.scss │ │ │ │ ├── nav.scss │ │ │ │ ├── news.scss │ │ │ │ └── tables.scss │ │ │ ├── callouts.scss │ │ │ ├── colors.scss │ │ │ ├── footer.scss │ │ │ ├── header.scss │ │ │ ├── headings.scss │ │ │ ├── lists.scss │ │ │ ├── nav.scss │ │ │ ├── sidebar.scss │ │ │ ├── syntax.scss │ │ │ ├── tables.scss │ │ │ └── tabs.scss │ │ ├── print.scss │ │ ├── search.scss │ │ └── style.scss │ └── templates │ │ ├── aip-listing.html.j2 │ │ ├── aip.html.j2 │ │ ├── includes │ │ ├── breadcrumb.html.j2 │ │ ├── footer.html.j2 │ │ ├── header.html.j2 │ │ ├── nav.html.j2 │ │ └── state_banners │ │ │ ├── draft.html.j2 │ │ │ ├── rejected.html.j2 │ │ │ ├── replaced.html.j2 │ │ │ ├── reviewing.html.j2 │ │ │ └── withdrawn.html.j2 │ │ ├── index.html.j2 │ │ ├── layouts │ │ └── base.html.j2 │ │ ├── page.html.j2 │ │ ├── redirect.html.j2 │ │ ├── search.html.j2 │ │ └── search.js.j2 └── utils.py ├── setup.py └── tests ├── conftest.py ├── test_cli.py ├── test_data ├── CONTRIBUTING.md ├── aip │ ├── general │ │ ├── 0031.md │ │ ├── 0038 │ │ │ ├── aip.md │ │ │ └── aip.yaml │ │ ├── 0043.md │ │ ├── 0059.md │ │ ├── 0062 │ │ │ ├── aip.en.md.j2 │ │ │ ├── aip.md.j2 │ │ │ ├── aip.yaml │ │ │ ├── les_mis.oas.yaml │ │ │ └── les_mis.proto │ │ ├── red-herring │ │ └── scope.yaml │ ├── poetry │ │ ├── 1609.md │ │ ├── 1622.md │ │ └── scope.yaml │ └── red-herring ├── config │ ├── ext.yaml │ ├── header.yaml │ ├── hero.yaml │ └── urls.yaml └── pages │ ├── authors │ ├── dickens.md │ └── hugo.md │ ├── general │ ├── faq.md.j2 │ ├── licensing.md │ ├── licensing.yaml │ └── page.md │ └── red-herring ├── test_jinja_ext_sample.py ├── test_jinja_ext_tab.py ├── test_jinja_loaders.py ├── test_md.py ├── test_models_aip.py ├── test_models_page.py ├── test_models_scope.py ├── test_models_site.py └── test_publisher.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | # Testing the development server is both difficult and unnecessary. 5 | aip_site/server.py 6 | 7 | [report] 8 | fail_under = 100 9 | show_missing = True 10 | exclude_lines = 11 | # Re-enable the standard pragma 12 | pragma: NO COVER 13 | 14 | # Enable an English version. 15 | Impossible; skip coverage checks. 16 | 17 | # Ignore debug-only repr 18 | def __repr__ 19 | 20 | # Abstract methods by definition are not invoked 21 | @abstractmethod 22 | @abc.abstractmethod 23 | 24 | # Type checking code will never be reached. 25 | if typing.TYPE_CHECKING: 26 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | # Closing bracket mismatches opening bracket's line. 4 | # This works poorly with type annotations in method declarations. 5 | E123, E124 6 | # Line over-indented for visual indent. 7 | # This works poorly with type annotations in method declarations. 8 | E126, E128, E131 9 | # Line break after binary operator. 10 | # This catches line breaks after "and" / "or" as a means of breaking up 11 | # long if statements, which PEP 8 explicitly encourages. 12 | W504 13 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # The AIP infrastructure team manages this repository. 2 | * @aip-dev/infra 3 | -------------------------------------------------------------------------------- /.github/release_notes.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import collections 16 | import subprocess 17 | import sys 18 | import typing 19 | 20 | 21 | class Changelog: 22 | def __init__(self, git_log: str): 23 | self._commits = {} 24 | for commit in git_log.split('\n'): 25 | # Spilt the type from the message. 26 | type_ = 'other' 27 | message = commit 28 | if ':' in message: 29 | type_, message = commit.split(':', 1) 30 | 31 | # If the change is breaking, note it separately. 32 | if type_.endswith('!'): 33 | type_ = 'breaking' 34 | 35 | # Add the commit to the appropriate bucket. 36 | self._commits.setdefault(type_, []) 37 | self._commits[type_].append(message.strip()) 38 | 39 | @property 40 | def markdown(self): 41 | """Return the changelog Markdown for the GitHub release.""" 42 | answer = '' 43 | headers = collections.OrderedDict(( 44 | ('breaking', 'Breaking Changes'), 45 | ('feat', 'Features'), 46 | ('fix', 'Bugfixes'), 47 | ('refactor', 'Refactors'), 48 | ('docs', 'Documentation'), 49 | )) 50 | for key, header in headers.items(): 51 | if key in self._commits: 52 | answer += f'## {header}\n\n' 53 | for cmt in self._commits[key]: 54 | answer += f'- {cmt}\n' 55 | answer += '\n' 56 | return answer.strip() 57 | 58 | 59 | def exec(cmd: typing.List[str]) -> str: 60 | """Execute the given command and return the output. 61 | 62 | If the command returns a non-zero exit status, fail loudly and exit. 63 | """ 64 | proc = subprocess.run(cmd, capture_output=True, text=True) 65 | if proc.returncode != 0: 66 | print(f'Error running {cmd[0]}: {proc.stderr}', file=sys.stderr) 67 | sys.exit(proc.returncode) 68 | return proc.stdout.strip() 69 | 70 | 71 | if __name__ == '__main__': 72 | # Get the previous tag and the current tag. 73 | revs = exec(['git', 'rev-list', '--simplify-by-decoration', 74 | '--tags', '--max-count=2']).split('\n') 75 | new_tag, prev_tag = (exec(['git', 'describe', '--tags', r]) for r in revs) 76 | commit_range = f'{prev_tag}..{new_tag}' 77 | if len(sys.argv) > 1: 78 | commit_range = sys.argv[1] 79 | 80 | # Get the changelog between those two tags. 81 | cl = Changelog(exec(['git', 'log', commit_range, 82 | '--oneline', '--pretty=format:%s'])) 83 | 84 | # Print the Markdown using GitHub's special syntax. 85 | # 86 | # Note: %0A must be used for newline. 87 | # https://github.community/t/set-output-truncates-multiline-strings/16852 88 | print('::set-output name=release_notes::{rel_notes}'.format( 89 | rel_notes=cl.markdown.replace('\n', '%0A') 90 | )) 91 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['config:base'], 3 | commitMessagePrefix: 'chore: ', 4 | groupName: 'all', 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: release 3 | on: 4 | push: 5 | tags: v[0-9]+.[0-9]+.[0-9]+ 6 | jobs: 7 | inspect: 8 | runs-on: ubuntu-latest 9 | container: python:3.10 10 | steps: 11 | - uses: actions/checkout@v3 12 | with: 13 | fetch-depth: 0 14 | - name: Get the version from the tag. 15 | id: get_version 16 | run: echo ::set-output name=version::${GITHUB_REF:11} 17 | shell: bash 18 | # see https://github.com/actions/runner-images/issues/6775 19 | # newer versions of Git check ownership of multi-user repositories 20 | # and will fail if one attempts to run Git in the checkout. 21 | - name: Workaround dubious ownership Git security check 22 | id: workaround_dubious_ownership 23 | run: git config --global --add safe.directory /__w/site-generator/site-generator 24 | - name: Get the release notes from the previous release to this one. 25 | id: release_tool 26 | run: python ./.github/release_notes.py 27 | outputs: 28 | version: ${{ steps.get_version.outputs.version }} 29 | release_notes: ${{ steps.release_tool.outputs.release_notes }} 30 | github_release: 31 | runs-on: ubuntu-latest 32 | needs: inspect 33 | steps: 34 | - name: Create the GitHub release 35 | uses: actions/create-release@v1 36 | env: 37 | GITHUB_TOKEN: ${{ github.token }} 38 | with: 39 | tag_name: v${{ needs.inspect.outputs.version }} 40 | release_name: aip-site-generator ${{ needs.inspect.outputs.version }} 41 | body: ${{ needs.inspect.outputs.release_notes }} 42 | draft: false 43 | prerelease: false 44 | pypi_release: 45 | runs-on: ubuntu-latest 46 | container: python:3.10 47 | needs: 48 | - inspect 49 | - github_release 50 | steps: 51 | - uses: actions/checkout@v3 52 | - name: Install twine. 53 | run: pip install twine 54 | - name: Set the version number. 55 | run: | 56 | cat > VERSION < typing.Tuple[str, str, None]: 37 | # Find the appropriate AIP file. 38 | if template == 'generic': 39 | fn = os.path.join(self.aip.path, 'aip.md') 40 | else: 41 | view = template.split('.') 42 | while view: 43 | view_str = '.'.join(view) 44 | fn = os.path.join(self.aip.path, f'aip.{view_str}.md') 45 | if os.path.isfile(fn) or os.path.isfile(f'{fn}.j2'): 46 | break 47 | view = view[1:] 48 | 49 | # Sanity check: Does the file exist? 50 | # If not, raise an error. 51 | if not os.path.isfile(fn) and not os.path.isfile(f'{fn}.j2'): 52 | raise jinja2.TemplateNotFound( 53 | f'Could not find {template} template for AIP-{self.aip.id}.', 54 | ) 55 | 56 | # Are we loading a plain file or a Jinja2 template file? 57 | if os.path.isfile(f'{fn}.j2'): 58 | fn += '.j2' 59 | 60 | # Load the contents. 61 | with io.open(fn) as f: 62 | contents = f.read() 63 | 64 | # Add custom {% block %} tags corresponding to Markdown headings. 65 | # 66 | # Note: We only do this if the template does not already have 67 | # {% block %} tags to avoid either stomping over input, or creating 68 | # an invalid template. 69 | if not re.search(r'\{%-? block', contents): 70 | # Iterate over the individual components in the table 71 | # of contents and make each into a block. 72 | contents = MarkdownDocument(contents).blocked_content 73 | 74 | # Return the template information. 75 | return contents, fn, None 76 | 77 | def list_templates(self) -> typing.List[str]: 78 | answer = [] 79 | 80 | # We sort the files in the directory to read more specific 81 | # files first. 82 | exts_regex = r'(\.(j2|md|proto|oas|yaml))*$' 83 | for fn in sorted(os.listdir(self.aip.path), 84 | key=lambda p: len(re.sub(exts_regex, '', p).split('.')), 85 | reverse=True): 86 | 87 | # Each file may specify a view, which corresponds to a separate 88 | # template. 89 | view = '.'.join(re.sub(exts_regex, '', fn).split('.')[1:]) 90 | if view and view not in answer: 91 | answer.append(view) 92 | 93 | # There is always a generic view. 94 | answer.append('generic') 95 | return answer 96 | -------------------------------------------------------------------------------- /aip_site/md.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import re 16 | 17 | import markdown # type: ignore 18 | import pymdownx.highlight # type: ignore 19 | import pymdownx.superfences # type: ignore 20 | 21 | from aip_site.utils import cached_property 22 | 23 | 24 | class MarkdownDocument(str): 25 | """A utility class representing Markdown content.""" 26 | 27 | def __init__(self, content: str, *, toc_title: str = 'Contents'): 28 | self._content = content 29 | self._engine = markdown.Markdown( 30 | extensions=[_superfences, 'tables', 'toc', 'pymdownx.tabbed'], 31 | extension_configs={ 32 | 'toc': { 33 | 'title': toc_title, 34 | 'toc_depth': '2-3', 35 | }, 36 | }, 37 | tab_length=2, 38 | ) 39 | 40 | def __str__(self) -> str: 41 | return self._content 42 | 43 | @cached_property 44 | def blocked_content(self) -> str: 45 | """Return a Markdown document with Jinja blocks added per-section.""" 46 | answer = str(self) 47 | 48 | # We need to fire the html property because conversion may not have 49 | # happened yet, and the toc extension requires conversion to occur 50 | # first. 51 | # 52 | # The `html` property caches, so this does not entail any additional 53 | # performance hit. 54 | self.html 55 | 56 | # We use the Markdown toc plugin to separate the headers and identify 57 | # where to put the blocks. Iterate over each toc_token and encapsulate 58 | # it in a block. 59 | toc_tokens = self._engine.toc_tokens # type: ignore 60 | for ix, token in enumerate(toc_tokens): 61 | try: 62 | next_token = toc_tokens[ix + 1] 63 | except IndexError: 64 | next_token = None 65 | answer = _add_block(answer, token, next_token) 66 | 67 | # Done; return the answer. 68 | return answer 69 | 70 | @cached_property 71 | def html(self) -> str: 72 | """Return the HTML content for this Markdown document.""" 73 | return self._engine.convert(self._content) 74 | 75 | @cached_property 76 | def title(self) -> str: 77 | """Return the document title.""" 78 | return self._content.strip().splitlines()[0].strip('# \n') 79 | 80 | @property 81 | def toc(self) -> str: 82 | """Return a table of contents for the Markdown document.""" 83 | # We need to fire the html property because conversion may not have 84 | # happened yet, and the toc extension requires conversion to occur 85 | # first. 86 | # 87 | # The `html` property caches, so this does not entail any additional 88 | # performance hit. 89 | self.html 90 | return self._engine.toc # type: ignore 91 | 92 | 93 | def _add_block(content: str, toc_token, next_toc_token=None) -> str: 94 | # Determine the beginning of the block. 95 | heading = '#' * toc_token['level'] + r'\s+' + toc_token['name'] 96 | match = re.search(heading, content) 97 | if not match: 98 | return content 99 | start_ix = match.span()[0] 100 | 101 | # Determine the end of the block. 102 | end_ix = len(content) 103 | if next_toc_token: 104 | end_h = '#' * next_toc_token['level'] + r'\s+' + next_toc_token['name'] 105 | match = re.search(end_h, content) 106 | if not match: 107 | return content 108 | end_ix = match.span()[0] 109 | elif '{% endblock %}' in content[start_ix:]: 110 | # We can match against an {% endblock %} safely because we are 111 | # processing "top-down" (e.g.

before

), then beginning-to-end; 112 | # this means that encountering an {% endblock %} is guaranteed to be 113 | # the endblock for the enclosing block. 114 | end_ix = content.index('{% endblock %}', start_ix) 115 | 116 | # Add the block for this heading. 117 | block = content[start_ix:end_ix] 118 | block_id = toc_token['id'].replace('-', '_') 119 | content = content.replace(block, '\n'.join(( 120 | f'{{% block {block_id} %}}', 121 | block, 122 | f'{{% endblock %}} {{# {block_id} #}}', 123 | )), 1) 124 | 125 | # Iterate over any children to this toc_token and process them. 126 | for ix, child in enumerate(toc_token['children']): 127 | try: 128 | next_child = toc_token['children'][ix + 1] 129 | except IndexError: 130 | next_child = None 131 | content = _add_block(content, child, next_toc_token=next_child) 132 | 133 | # Done; return the content with blocks. 134 | return content 135 | 136 | 137 | def _fmt(src: str, lang: str, css_class: str, *args, **kwargs): 138 | """Custom Markdown formatter that retains language classes.""" 139 | highlighter = pymdownx.highlight.Highlight(guess_lang=False) 140 | return highlighter.highlight(src, lang, f'{css_class} language-{lang}') 141 | 142 | 143 | # Define the superfences extension. 144 | # We need a custom fence to maintain useful language-specific CSS classes. 145 | _superfences = pymdownx.superfences.SuperFencesCodeExtension( 146 | custom_fences=[{ 147 | 'name': '*', 148 | 'class': 'highlight', 149 | 'format': _fmt, 150 | }], 151 | ) 152 | -------------------------------------------------------------------------------- /aip_site/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aip-dev/site-generator/082c5f7e12a265f9c2f25c1bdff08cc0f8aaa65c/aip_site/models/__init__.py -------------------------------------------------------------------------------- /aip_site/models/aip.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import annotations 16 | import collections 17 | import dataclasses 18 | import datetime 19 | import io 20 | import re 21 | import typing 22 | 23 | import jinja2 24 | 25 | from aip_site import md 26 | from aip_site.jinja.env import jinja_env 27 | from aip_site.jinja.ext.sample import SampleExtension 28 | from aip_site.jinja.ext.tab import TabExtension 29 | from aip_site.jinja import loaders 30 | from aip_site.utils import cached_property 31 | 32 | 33 | @dataclasses.dataclass(frozen=True) 34 | class AIP: 35 | id: int 36 | state: str 37 | created: datetime.date 38 | scope: Scope 39 | path: str 40 | config: typing.Dict[str, typing.Any] 41 | changelog: typing.Set[Change] = dataclasses.field(default_factory=set) 42 | 43 | @cached_property 44 | def content(self) -> md.MarkdownDocument: 45 | # Render the content template into the actual content. 46 | answer = next(iter(self.templates.values())).render( 47 | aip=self, 48 | site=self.site, 49 | ) 50 | 51 | # TEMPORARY: In the interest of having two discrete migrations, 52 | # rewrite the old link style to the new one. 53 | answer = re.sub( 54 | r'\.?\./0([\d]{1,3})\.md', 55 | lambda m: f'{self.site.relative_uri}/{int(m.groups()[0]):d}', 56 | answer, 57 | ) 58 | 59 | # Hotlink AIP references. 60 | # We can (now) link to AIPs outside of general, those that are numbered 61 | # > 999, because the site redirects from 4221 to client-libraries/4221 62 | # now, for example. 63 | answer = re.sub( 64 | r'\b(? jinja2.Environment: 85 | return jinja2.Environment( 86 | extensions=[ 87 | SampleExtension, 88 | TabExtension, 89 | ], 90 | loader=loaders.AIPLoader(self), 91 | undefined=jinja2.StrictUndefined, 92 | ) 93 | 94 | @property 95 | def placement(self) -> Placement: 96 | """Return the placement data for this AIP, or sensible defaults.""" 97 | p = self.config.get('placement', {}) 98 | p.setdefault('category', self.scope.code) 99 | return Placement(**p) 100 | 101 | @property 102 | def redirects(self) -> typing.Set[str]: 103 | uris = {f'/{self.id:04d}', f'/{self.id:03d}', f'/{self.id:02d}'} 104 | uris = uris.difference({self.relative_uri}) 105 | if 'redirect_from' in self.config: 106 | uris.add(self.config['redirect_from']) 107 | return uris 108 | 109 | @property 110 | def relative_uri(self) -> str: 111 | """Return the relative URI for this AIP.""" 112 | if self.scope.code != 'general': 113 | return f'/{self.scope.code}/{self.id}' 114 | return f'/{self.id}' 115 | 116 | @property 117 | def repo_path(self) -> str: 118 | """Return the relative repository path.""" 119 | return self.path[len(self.site.base_dir):] 120 | 121 | @property 122 | def site(self) -> Site: 123 | """Return the site for this AIP.""" 124 | return self.scope.site 125 | 126 | @cached_property 127 | def templates(self) -> typing.Dict[str, jinja2.Template]: 128 | """Load and return the templates for this AIP.""" 129 | # Sanity check: Is this a legacy AIP (in the old Jekyll format)? 130 | # If so, process out a single template and return it. 131 | if self._legacy: 132 | with io.open(self.path, 'r') as f: 133 | contents = f.read() 134 | _, body = contents.lstrip('-\n').split('---\n', maxsplit=1) 135 | return {'generic': jinja2.Template(body, 136 | undefined=jinja2.StrictUndefined, 137 | )} 138 | 139 | # Return a dictionary with all of the templates. 140 | # 141 | # Note: This could be made more efficient in the future by only loading 142 | # the template that the user wants. 143 | return collections.OrderedDict([ 144 | (k, self.env.get_template(k)) for k in self.env.list_templates() 145 | ]) 146 | 147 | @cached_property 148 | def title(self) -> str: 149 | """Return the AIP title.""" 150 | return self.content.title 151 | 152 | @property 153 | def updated(self): 154 | """Return the most recent date on the changelog. 155 | 156 | If there is no changelog, return the created date instead. 157 | """ 158 | if self.changelog: 159 | return sorted(self.changelog)[0].date 160 | return self.created 161 | 162 | @property 163 | def _legacy(self) -> bool: 164 | """Return True if this is a legacy AIP, False otherwise.""" 165 | return self.path.endswith('.md') 166 | 167 | def render(self): 168 | """Return the fully-rendered page for this AIP.""" 169 | return jinja_env.get_template('aip.html.j2').render( 170 | aip=self, 171 | path=self.relative_uri, 172 | site=self.site, 173 | ) 174 | 175 | 176 | @dataclasses.dataclass(frozen=True) 177 | class Change: 178 | date: datetime.date 179 | message: str 180 | 181 | def __hash__(self): 182 | return hash(str(self)) 183 | 184 | def __lt__(self, other: 'Change'): 185 | # We sort changes in reverse-cron, so a later date is "less" here. 186 | return self.date > other.date 187 | 188 | def __str__(self): 189 | return f'{self.date.isoformat()}: {self.message}' 190 | 191 | 192 | @dataclasses.dataclass(frozen=True) 193 | class Placement: 194 | category: str 195 | order: typing.Union[int, float] = float('inf') 196 | 197 | 198 | if typing.TYPE_CHECKING: 199 | from aip_site.models.scope import Scope 200 | from aip_site.models.site import Site 201 | -------------------------------------------------------------------------------- /aip_site/models/page.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import annotations 16 | import dataclasses 17 | import io 18 | import re 19 | import os 20 | import typing 21 | 22 | import yaml 23 | 24 | from aip_site import md 25 | from aip_site.jinja.env import jinja_env 26 | from aip_site.utils import cached_property 27 | 28 | 29 | @dataclasses.dataclass(frozen=True) 30 | class Page: 31 | body: md.MarkdownDocument 32 | repo_path: str 33 | collection: Collection 34 | config: typing.Dict[str, typing.Any] 35 | 36 | @property 37 | def code(self) -> str: 38 | """Return this page's code. 39 | 40 | Note: Uniqueness is only guaranteed within the collection. 41 | """ 42 | return self.repo_path.split('/')[-1].split('.')[0].lower() 43 | 44 | @cached_property 45 | def content(self) -> md.MarkdownDocument: 46 | """Return the Markdown content for this page.""" 47 | # If this was originally a Jinja template, then we need to render 48 | # this specific content in isolation (apart from the page template). 49 | if self.repo_path.endswith('.j2'): 50 | return md.MarkdownDocument( 51 | jinja_env.from_string(self.body).render(site=self.site), 52 | ) 53 | 54 | # This is not a template; just return the body directly. 55 | return self.body 56 | 57 | @property 58 | def relative_uri(self) -> str: 59 | return f'/{self.collection.code}/{self.code}'.replace('/general', '') 60 | 61 | @property 62 | def site(self) -> Site: 63 | return self.collection.site 64 | 65 | @property 66 | def title(self) -> str: 67 | """Return the page title.""" 68 | return self.content.title 69 | 70 | def render(self) -> str: 71 | return jinja_env.get_template('page.html.j2').render( 72 | page=self, 73 | path=self.relative_uri, 74 | site=self.site, 75 | ) 76 | 77 | 78 | @dataclasses.dataclass(frozen=True) 79 | class Collection: 80 | code: str 81 | site: Site 82 | 83 | @property 84 | def base_dir(self) -> str: 85 | return os.path.join(self.site.base_dir, 'pages', self.code) 86 | 87 | @cached_property 88 | def pages(self) -> typing.Dict[str, Page]: 89 | """Return the pages in this collection.""" 90 | answer = {} 91 | 92 | # For the general collection, the CONTRIBUTING.md file is a special 93 | # case: we need it in the root so GitHub will find it. 94 | if self.code == 'general': 95 | answer['contributing'] = self._load_page( 96 | os.path.join(self.site.base_dir, 'CONTRIBUTING.md'), 97 | ) 98 | 99 | # Iterate over the pages directory and load static pages. 100 | for fn in os.listdir(self.base_dir): 101 | # Sanity check: Ignore non-Markdown files. 102 | if not fn.endswith(('.md', '.md.j2')): 103 | continue 104 | 105 | # Load the page and add it to the pages dictionary. 106 | page = self._load_page(os.path.join(self.base_dir, fn)) 107 | answer[page.code] = page 108 | return answer 109 | 110 | def _load_page(self, md_file: str) -> Page: 111 | """Load a support page and return a new Page object.""" 112 | # Read the page file from disk. 113 | with io.open(md_file, 'r') as f: 114 | body = f.read() 115 | 116 | # Check for a config file. If one exists, load it too. 117 | config_file = re.sub(r'\.md(\.j2)?$', '.yaml', md_file) 118 | config = {} 119 | if os.path.exists(config_file): 120 | with io.open(config_file, 'r') as f: 121 | config = yaml.safe_load(f) 122 | 123 | # Return the page. 124 | return Page( 125 | body=md.MarkdownDocument(body), 126 | collection=self, 127 | config=config, 128 | repo_path=md_file[len(self.site.base_dir):], 129 | ) 130 | 131 | 132 | if typing.TYPE_CHECKING: 133 | from aip_site.models.site import Site 134 | -------------------------------------------------------------------------------- /aip_site/models/scope.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import annotations 16 | import collections 17 | import dataclasses 18 | import io 19 | import os 20 | import typing 21 | 22 | import yaml 23 | 24 | from aip_site.jinja.env import jinja_env 25 | from aip_site.models.aip import AIP 26 | from aip_site.models.aip import Change 27 | from aip_site.utils import cached_property 28 | 29 | 30 | @dataclasses.dataclass(frozen=True) 31 | class Scope: 32 | base_dir: str 33 | code: str 34 | title: str 35 | order: int 36 | site: Site 37 | config: typing.Dict[str, typing.Any] 38 | 39 | @cached_property 40 | def aips(self) -> typing.Dict[int, AIP]: 41 | """Return a dict of AIPs, sorted by number.""" 42 | answer = [] 43 | 44 | # Load each AIP under this scope. 45 | # Similar to scopes, AIPs are detected based solely on 46 | # presence of file on disk. 47 | for fn in os.listdir(self.base_dir): 48 | path = os.path.join(self.base_dir, fn) 49 | aip_config = os.path.join(path, 'aip.yaml') 50 | if os.path.isdir(path) and os.path.exists(aip_config): 51 | with io.open(aip_config, 'r') as f: 52 | meta = yaml.safe_load(f) 53 | 54 | # Sanity check: Does this look like an old-style AIP? 55 | elif path.endswith('.md'): 56 | # Load the AIP. 57 | with io.open(path, 'r') as f: 58 | contents = f.read() 59 | 60 | # Parse out the front matter from the Markdown content. 61 | fm, body = contents.lstrip('-\n').split('---\n', maxsplit=1) 62 | meta = yaml.safe_load(fm) 63 | 64 | # This is not a recognized AIP. 65 | else: 66 | meta = None # Not needed; fixes a coverage bug. 67 | continue 68 | 69 | # Create the AIP object. 70 | answer.append(AIP( 71 | id=meta.pop('id'), 72 | config=meta, 73 | changelog={Change(**i) for i in meta.pop('changelog', [])}, 74 | created=meta.pop('created'), 75 | path=path, 76 | scope=self, 77 | state=meta.pop('state'), 78 | )) 79 | 80 | answer = sorted(answer, key=lambda i: i.id) 81 | return collections.OrderedDict([(i.id, i) for i in answer]) 82 | 83 | @cached_property 84 | def categories(self) -> typing.Dict[str, Category]: 85 | # There may be no categories. This is fine; just "fake" a single 86 | # category based on the scope. 87 | # 88 | # This is guaranteed to have all AIPs, so just return immediately 89 | # and skip remaining processing. 90 | if not self.config.get('categories', None): 91 | return {self.code: Category( 92 | code=self.code, 93 | title=self.title, 94 | order=0, 95 | aips=self.aips, 96 | )} 97 | 98 | # Iterate over each category and piece together the AIPs in order. 99 | cats = collections.OrderedDict() 100 | for ix, cat in enumerate(self.config['categories']): 101 | # Determine what AIPs are in this category. 102 | aips: typing.List[AIP] = [] 103 | for aip in self.aips.values(): 104 | if aip.placement.category != cat['code']: 105 | continue 106 | aips.append(aip) 107 | aips = sorted(aips, key=lambda i: i.placement.order) 108 | 109 | # Create the category object and add it. 110 | cats[cat['code']] = Category( 111 | code=cat['code'], 112 | title=cat.get('title', cat['code'].capitalize()), 113 | order=ix, 114 | aips=collections.OrderedDict([(i.id, i) for i in aips]), 115 | ) 116 | return cats 117 | 118 | @property 119 | def relative_uri(self) -> str: 120 | return f'/{self.code}' 121 | 122 | def render(self): 123 | """Render the page for this scope.""" 124 | return jinja_env.get_template('aip-listing.html.j2').render( 125 | path=self.relative_uri, 126 | scope=self, 127 | site=self.site, 128 | ) 129 | 130 | 131 | @dataclasses.dataclass(frozen=True) 132 | class Category: 133 | code: str 134 | title: str 135 | order: int 136 | aips: typing.Dict[int, AIP] 137 | 138 | 139 | if typing.TYPE_CHECKING: 140 | from aip_site.models.site import Site 141 | -------------------------------------------------------------------------------- /aip_site/models/site.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import annotations 16 | import collections 17 | import dataclasses 18 | import io 19 | import os 20 | import typing 21 | import uuid 22 | 23 | import yaml 24 | 25 | from aip_site.models.aip import AIP 26 | from aip_site.models.page import Collection 27 | from aip_site.models.page import Page 28 | from aip_site.models.scope import Scope 29 | from aip_site.utils import cached_property 30 | 31 | 32 | @dataclasses.dataclass(frozen=True) 33 | class Site: 34 | base_dir: str 35 | revision: str 36 | config: typing.Dict[str, typing.Any] 37 | 38 | @classmethod 39 | def load(cls, base_dir: str) -> Site: 40 | # Load all site configuration. 41 | config: typing.Dict[str, typing.Any] = {} 42 | config_dir = os.path.join(base_dir, 'config') 43 | for fn in os.listdir(config_dir): 44 | with io.open(os.path.join(config_dir, fn), 'r') as f: 45 | config[fn[:-5]] = yaml.safe_load(f) 46 | 47 | # Return a new Site object. 48 | return cls( 49 | base_dir=base_dir, 50 | config=config, 51 | revision=str(uuid.uuid4())[-8:], 52 | ) 53 | 54 | @cached_property 55 | def aips(self) -> typing.Dict[int, AIP]: 56 | """Return all the AIPs in the site.""" 57 | answer = collections.OrderedDict() 58 | for scope in self.scopes.values(): 59 | for id, aip in scope.aips.items(): 60 | answer[id] = aip 61 | return answer 62 | 63 | @property 64 | def base_url(self) -> str: 65 | """Return the site's base URL.""" 66 | return self.config.get('urls', {}).get('site', '').rstrip('/') 67 | 68 | @cached_property 69 | def collections(self) -> typing.Dict[str, Collection]: 70 | """Return all of the page collections in the site.""" 71 | pages_dir = os.path.join(self.base_dir, 'pages') 72 | answer = {} 73 | for dirname in os.listdir(pages_dir): 74 | # Sanity check: Is this a directory? 75 | if not os.path.isdir(os.path.join(pages_dir, dirname)): 76 | continue 77 | answer[dirname] = Collection(code=dirname, site=self) 78 | return answer 79 | 80 | @property 81 | def pages(self) -> typing.Dict[str, Page]: 82 | """Return all of the static pages in the site.""" 83 | answer = {} 84 | for col in self.collections.values(): 85 | for page in col.pages.values(): 86 | # Disambiguate the pages from one another, but treat the 87 | # general collection as special and remove the prefix. 88 | code = f'{col.code}/{page.code}'.replace('general/', '') 89 | answer[code] = page 90 | return answer 91 | 92 | @property 93 | def relative_uri(self) -> str: 94 | return '/{}'.format('/'.join(self.base_url.split('/')[3:])).rstrip('/') 95 | 96 | @property 97 | def repo_url(self) -> str: 98 | return self.config.get('urls', {}).get('repo', '').rstrip('/') 99 | 100 | @cached_property 101 | def scopes(self) -> typing.Dict[str, Scope]: 102 | """Return all of the AIP scopes present in the site.""" 103 | answer_list = [] 104 | aip_dir = os.path.join(self.base_dir, 'aip') 105 | for fn in os.listdir(aip_dir): 106 | # If there is a scope.yaml file, then this is a scope directory 107 | # and we add it to our list. 108 | scope_file = os.path.join(aip_dir, fn, 'scope.yaml') 109 | if not os.path.exists(scope_file): 110 | continue 111 | 112 | # Open the scope's configuration file and create a Scope 113 | # object based on it. 114 | with io.open(scope_file, 'r') as f: 115 | conf = yaml.safe_load(f) 116 | code = conf.pop('code', fn) 117 | scope = Scope( 118 | base_dir=os.path.join(aip_dir, fn), 119 | code=code, 120 | config=conf, 121 | order=conf.pop('order', float('inf')), 122 | site=self, 123 | title=conf.pop('title', code.capitalize()), 124 | ) 125 | 126 | # Append the scope to our list. 127 | answer_list.append(scope) 128 | 129 | # Create an ordered dictionary of scopes. 130 | answer = collections.OrderedDict() 131 | for scope in sorted(answer_list, key=lambda i: i.order): 132 | answer[scope.code] = scope 133 | return answer 134 | -------------------------------------------------------------------------------- /aip_site/publisher.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from __future__ import annotations 16 | import io 17 | import os 18 | import re 19 | import shutil 20 | import typing 21 | 22 | from scss.compiler import compile_file # type: ignore 23 | import click 24 | 25 | from aip_site.jinja.env import jinja_env 26 | from aip_site.models.aip import AIP 27 | from aip_site.models.scope import Scope 28 | from aip_site.models.site import Site 29 | from aip_site.models.page import Page 30 | 31 | 32 | SUPPORT_ROOT = os.path.realpath( 33 | os.path.join(os.path.dirname(__file__), 'support'), 34 | ) 35 | 36 | 37 | class Publisher: 38 | """Class for writing the AIP site out to disk.""" 39 | 40 | def __init__(self, src, dest): 41 | self.output_dir = dest.rstrip(os.path.sep) 42 | self.site = Site.load(src.rstrip(os.path.sep)) 43 | 44 | def log(self, message: str, *, err: bool = False): 45 | """Conditionally log a message.""" 46 | click.secho(f'{message}', nl=True, err=err) 47 | 48 | def publish_site(self): 49 | """Publish the full site.""" 50 | # Publish every page, AIP listing, and AIP. 51 | for page in self.site.pages.values(): 52 | self.publish_doc(page) 53 | for scope in self.site.scopes.values(): 54 | self.publish_doc(scope) 55 | for aip in self.site.aips.values(): 56 | self.publish_doc(aip) 57 | 58 | # Publish support pages that are not Markdown pages. 59 | self.publish_template('index.html.j2', 'index.html') 60 | self.publish_template('search.html.j2', 'search.html') 61 | 62 | # Copy over assets files verbatim. 63 | self.publish_assets() 64 | 65 | # Publish dynamic content that goes in the assets directory. 66 | self.publish_template( 67 | 'search.js.j2', 68 | 'assets/js/search/tipuesearch_content.js', 69 | ) 70 | self.publish_css() 71 | 72 | def publish_doc(self, doc: typing.Union[AIP, Scope, Page]): 73 | """Publish a single AIP document to disk.""" 74 | # Determine the path and make sure the directory exists. 75 | path = f'{self.output_dir}{doc.relative_uri}.html' 76 | os.makedirs(os.path.dirname(path), exist_ok=True) 77 | 78 | # Write the document's file to disk. 79 | with io.open(path, 'w') as f: 80 | f.write(doc.render()) 81 | self.log(f'Successfully wrote {path}.') 82 | 83 | # If this document has any redirects (only AIPs do as of this writing), 84 | # write out the redirect files to disk also. 85 | for redirect in getattr(doc, 'redirects', set()): 86 | rpath = f'{self.output_dir}{redirect}.html' 87 | with io.open(rpath, 'w') as f: 88 | f.write(jinja_env.get_template('redirect.html.j2').render( 89 | site=self.site, 90 | target=doc, 91 | )) 92 | self.log(f'Successfully wrote {rpath} (redirect).') 93 | 94 | def publish_template(self, tmpl: str, path: str): 95 | """Publish a site-wide template to a particular path.""" 96 | path = f'{self.output_dir}{os.path.sep}{path}' 97 | os.makedirs(os.path.dirname(path), exist_ok=True) 98 | with io.open(path, 'w') as f: 99 | f.write(jinja_env.get_template(tmpl).render( 100 | site=self.site, 101 | path=path.split('.')[0], 102 | )) 103 | self.log(f'Successfully wrote {path}.') 104 | 105 | def publish_assets(self): 106 | """Copy the assets directory.""" 107 | shutil.copytree( 108 | src=os.path.join(SUPPORT_ROOT, 'assets'), 109 | dst=os.path.join(self.output_dir, 'assets'), 110 | dirs_exist_ok=True, 111 | ) 112 | self.log('Successfully wrote asset files.') 113 | 114 | def publish_css(self): 115 | """Compile and publish all SCSS files in the root SCSS directory.""" 116 | scss_path = os.path.join(SUPPORT_ROOT, 'scss') 117 | css_path = os.path.join(self.output_dir, 'assets', 'css') 118 | os.makedirs(css_path, exist_ok=True) 119 | for scss_file in os.listdir(scss_path): 120 | if not scss_file.endswith('.scss'): 121 | continue 122 | css_file = os.path.join( 123 | css_path, 124 | re.sub(r'\.scss$', '.css', scss_file), 125 | ) 126 | with io.open(css_file, 'w') as f: 127 | f.write(compile_file(os.path.join(scss_path, scss_file))) 128 | self.log(f'Successfully wrote {css_file}.') 129 | -------------------------------------------------------------------------------- /aip_site/server.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | import re 17 | 18 | from scss.compiler import compile_file # type: ignore 19 | import flask 20 | 21 | from aip_site.jinja.env import jinja_env 22 | from aip_site.models.site import Site 23 | 24 | 25 | ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__), '..')) 26 | app = flask.Flask('aip', static_folder=f'{ROOT}/aip_site/support/assets/') 27 | app.jinja_env = jinja_env # type: ignore 28 | 29 | 30 | @app.route('/') 31 | def hello(): 32 | return flask.render_template('index.html.j2', site=flask.g.site) 33 | 34 | 35 | @app.route('/') 36 | def aip(aip_id: int): 37 | """Display a single AIP document.""" 38 | return flask.g.site.aips[aip_id].render() 39 | 40 | 41 | @app.route('/search') 42 | def search(): 43 | """Display the search page.""" 44 | return flask.render_template('search.html.j2', 45 | site=flask.g.site, 46 | path=flask.request.path, 47 | ) 48 | 49 | 50 | @app.route('/') 51 | @app.route('//') 52 | def page(page: str, collection: str = "general"): 53 | """Display a static page or listing of AIPs in a given scope.""" 54 | site = flask.g.site 55 | if page in site.scopes: 56 | return site.scopes[page].render() 57 | if ( 58 | collection in site.collections and 59 | page in site.collections[collection].pages 60 | ): 61 | return site.collections[collection].pages[page].render() 62 | if collection in site.scopes and int(page) in site.scopes[collection].aips: 63 | return site.scopes[collection].aips[int(page)].render() 64 | flask.abort(404) 65 | 66 | 67 | @app.route('/assets/css/') 68 | def scss(css_file: str): 69 | """Compile the given SCSS file and return it.""" 70 | scss_file = re.sub(r'\.css$', '.scss', css_file) 71 | css = compile_file(f'{ROOT}/aip_site/support/scss/{scss_file}') 72 | return flask.Response(css, mimetype='text/css') 73 | 74 | 75 | @app.route('/assets/js/search/tipuesearch_content.js') 76 | def search_content(): 77 | """Compile the search content JavaScript and return it.""" 78 | return flask.Response( 79 | flask.render_template('search.js.j2', site=flask.g.site), 80 | mimetype='text/javascript', 81 | ) 82 | 83 | 84 | def site_load_func(src: str): 85 | """Return a function that loads the site and registers it. 86 | 87 | This is used in cli.py to register to Flask.before_request. 88 | """ 89 | def fx(): 90 | flask.g.site = Site.load(src) 91 | 92 | # This is the dev server, so plow over whatever the configuration 93 | # says that the site URL is. 94 | flask.g.site.config.setdefault('urls', {}) 95 | flask.g.site.config['urls']['site'] = 'http://localhost:4000' 96 | return fx 97 | -------------------------------------------------------------------------------- /aip_site/support/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aip-dev/site-generator/082c5f7e12a265f9c2f25c1bdff08cc0f8aaa65c/aip_site/support/assets/favicon.ico -------------------------------------------------------------------------------- /aip_site/support/assets/images/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aip-dev/site-generator/082c5f7e12a265f9c2f25c1bdff08cc0f8aaa65c/aip_site/support/assets/images/github.png -------------------------------------------------------------------------------- /aip_site/support/assets/js/aip/aip-graphviz.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file contains JavaScript-applied rules that render graphviz objects 16 | // in AIPs. 17 | // 18 | // Note: This script is not included in AIPs by default, because the scripts 19 | // needed to render graphviz are nearly 1.5 MB in size that including them 20 | // everywhere adds significant weight. 21 | // 22 | // See the front matter for AIP-1 or AIP-100 for examples of how to add this 23 | // for AIPs that need graphviz rendering. 24 | $.when($.ready).then(() => { 25 | $('.language-graphviz').each((_, el) => { 26 | let pre = $(el).children('pre'); 27 | let graphviz = $(el).find('code').text(); 28 | 29 | // Render a SVG and replace the original graphviz code with it. 30 | let viz = new Viz(); 31 | viz.renderSVGElement(graphviz).then((svg) => { 32 | $('

').append(svg).insertBefore(pre); 33 | pre.remove(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /aip_site/support/assets/js/aip/aip-index.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file contains JavaScript-applied rules that apply to the AIP index 16 | // specifically. 17 | $.when($.ready).then(() => { 18 | // The base README page should have tables that span the full width 19 | // and look consistent with one another. 20 | for (let topLeftCell of ['Number', 'Block']) { 21 | $(`table th:first-child:contains(${topLeftCell})`) 22 | .parents('table') 23 | .addClass('aip-listing'); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /aip_site/support/assets/js/global.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file contains JavaScript-applied rules that can be applied 16 | // to documentation sites using this Jekyll theme generally. 17 | $.when($.ready).then(() => { 18 | // Certain selectors should apply Glue classes. 19 | // (For our purposes, this essentially mimics the SCSS @extend directive.) 20 | // In general, we can not apply these directly because the tags are generated 21 | // while compiling from Markdown, so we do it ex post facto. 22 | let extend = new Map(); 23 | for (let h = 1; h <= 5; h += 1) { 24 | let classes = [ 25 | 'glue-headline', 26 | 'glue-has-top-margin', 27 | `glue-headline--headline-${h + 1}`, 28 | ]; 29 | if (h <= 3) { 30 | classes.push('glue-has-bottom-margin'); 31 | } 32 | extend.set(`.docs-component-main h${h}`, classes); 33 | } 34 | extend.set('table:not(.no-h)', ['glue-table']); 35 | extend.set('#aip-main table:not(.no-h)', ['glue-table--datatable']); 36 | extend.set('.tipue_search_content_title', [ 37 | 'glue-headline', 38 | 'glue-headline--headline-4', 39 | 'glue-has-top-margin', 40 | ]); 41 | for (let [selector, classes] of extend) { 42 | $(selector).addClass(classes); 43 | } 44 | 45 | // Make callouts for notes, warnings, etc. work. 46 | for (let callout of ['Important', 'Note', 'TL;DR', 'Warning', 'Summary']) { 47 | $(`p strong:contains(${callout}:)`) 48 | .parent() 49 | .addClass(callout.replace(';', '').toLowerCase()); 50 | } 51 | 52 | // Make "spec terms" (must, should, may, must not, should not) that 53 | // are bold-faced be further emphasized. 54 | for (let directive of ['may', 'must', 'must not', 'must\nnot', 55 | 'should', 'should not', 'should\nnot']) { 56 | $('strong') 57 | .filter((_, el) => $(el).text() === directive) 58 | .addClass('spec-directive') 59 | .addClass(`spec-${directive.split(' ')[0].split('\n')[0]}`); 60 | } 61 | 62 | // Make "reviewing sections" of approved AIPs show a badge. 63 | let reviewing = $('h3 + p').filter((_, el) => $(el).text() === '[^reviewing]'); 64 | reviewing.prev('h3').addClass('reviewing'); 65 | reviewing.remove(); 66 | 67 | // Make AIP banners appear in a better spot. 68 | $('#aip-state-banner').insertAfter('#aip-main h1'); 69 | 70 | // Control the maximum height of the nav sidebar. 71 | $(window) 72 | .on('resize', () => { 73 | $('nav.docs-component-nav').css({ 74 | maxHeight: `${$(window).height() - 110}px`, 75 | }); 76 | }) 77 | .resize(); 78 | }); 79 | -------------------------------------------------------------------------------- /aip_site/support/assets/js/search/pagination.js: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // This file contains JavaScript that makes the search boxes work better. 16 | // It causes the entire
  • to be clickable instead of just the link. 17 | // (We do not control the HTML so this is the best option.) 18 | $.when($.ready).then(() => { 19 | $(document).on( 20 | 'click', 21 | '#tipue_search_foot_boxes li:not(.current)', 22 | function () { 23 | $(this).children('a').eq(0).click(); 24 | } 25 | ); 26 | }); 27 | -------------------------------------------------------------------------------- /aip_site/support/assets/js/search/tipuesearch.min.js: -------------------------------------------------------------------------------- 1 | (function($){$.fn.tipuesearch=function(options){var set=$.extend({'contentLocation':'tipuesearch/tipuesearch_content.json','contextBuffer':60,'contextLength':60,'contextStart':90,'debug':false,'descriptiveWords':25,'highlightTerms':true,'liveContent':'*','liveDescription':'*','minimumLength':3,'mode':'static','newWindow':false,'show':9,'showContext':true,'showRelated':true,'showTime':true,'showTitleCount':true,'showURL':true,'wholeWords':true},options);return this.each(function(){var tipuesearch_in={pages:[]};$.ajaxSetup({async:false});var tipuesearch_t_c=0;$('#tipue_search_content').hide().html('
    ').show();if(set.mode=='live') 2 | {for(var i=0;i');var t_2=html.toLowerCase().indexOf('',t_1+7);if(t_1!=-1&&t_2!=-1) 5 | {var tit=html.slice(t_1+7,t_2);} 6 | else 7 | {var tit=tipuesearch_string_1;} 8 | tipuesearch_in.pages.push({"title":tit,"text":desc,"tags":cont,"url":tipuesearch_pages[i]});});}} 9 | if(set.mode=='json') 10 | {$.getJSON(set.contentLocation).done(function(json) 11 | {tipuesearch_in=$.extend({},json);});} 12 | if(set.mode=='static') 13 | {tipuesearch_in=$.extend({},tipuesearch);} 14 | var tipue_search_w='';if(set.newWindow) 15 | {tipue_search_w=' target="_blank"';} 16 | function getURLP(name) 17 | {var _locSearch=location.search;var _splitted=(new RegExp('[?|&]'+name+'='+'([^&;]+?)(&|#|;|$)').exec(_locSearch)||[,""]);var searchString=_splitted[1].replace(/\+/g,'%20');try 18 | {searchString=decodeURIComponent(searchString);} 19 | catch(e) 20 | {searchString=unescape(searchString);} 21 | return searchString||null;} 22 | if(getURLP('q')) 23 | {$('#tipue_search_input').val(getURLP('q'));getTipueSearch(0,true);} 24 | $(this).keyup(function(event) 25 | {if(event.keyCode=='13') 26 | {getTipueSearch(0,true);}});function getTipueSearch(start,replace) 27 | {var out='';var show_replace=false;var show_stop=false;var standard=true;var c=0;found=[];var d_o=$('#tipue_search_input').val();var d=d_o.toLowerCase();d=$.trim(d);if((d.match("^\"")&&d.match("\"$"))||(d.match("^'")&&d.match("'$"))) 28 | {standard=false;} 29 | var d_w=d.split(' ');if(standard) 30 | {d='';for(var i=0;i=set.minimumLength) 40 | {if(standard) 41 | {if(replace) 42 | {var d_r=d;for(var i=0;i'+d_r+'';} 95 | if(c==1) 96 | {out+='
    '+tipuesearch_string_4;} 97 | else 98 | {c_c=c.toString().replace(/\B(?=(\d{3})+(?!\d))/g,",");out+='
    '+c_c+' '+tipuesearch_string_5;} 99 | if(set.showTime) 100 | {var endTimer=new Date().getTime();var time=(endTimer-startTimer)/ 1000;out+=' ('+time.toFixed(2)+' '+tipuesearch_string_14+')';set.showTime=false;} 101 | out+='
    ';found.sort(function(a,b){return b.score-a.score});var l_o=0;for(var i=0;i=start&&l_o'+found[i].title+'
    ';if(set.debug) 104 | {out+='
    Score: '+found[i].score+'
    ';} 105 | if(set.showURL) 106 | {var s_u=found[i].url.toLowerCase();if(s_u.indexOf('http://')==0) 107 | {s_u=s_u.slice(7);} 108 | out+='';} 109 | if(found[i].desc) 110 | {var t=found[i].desc;if(set.showContext) 111 | {d_w=d.split(' ');var s_1=found[i].desc.toLowerCase().indexOf(d_w[0]);if(s_1>set.contextStart) 112 | {var t_1=t.substr(s_1-set.contextBuffer);var s_2=t_1.indexOf(' ');t_1=t.substr(s_1-set.contextBuffer+s_2);t_1=$.trim(t_1);if(t_1.length>set.contextLength) 113 | {t='... '+t_1;}}} 114 | if(standard) 115 | {d_w=d.split(' ');for(var f=0;f$1");}}} 118 | else if(set.highlightTerms) 119 | {var patr=new RegExp('('+d+')','gi');t=t.replace(patr,"$1");} 120 | var t_d='';var t_w=t.split(' ');if(t_w.length'+t_d+'';}} 128 | l_o++;} 129 | if(set.showRelated&&standard) 130 | {f=0;for(var i=0;i'+d_o+'';}} 143 | if(c>set.show) 144 | {var pages=Math.ceil(c / set.show);var page=(start / set.show);out+='';}} 165 | else 166 | {out+='
    '+tipuesearch_string_8+'
    ';}} 167 | else 168 | {if(show_stop) 169 | {out+='
    '+tipuesearch_string_8+'. '+tipuesearch_string_9+'
    ';} 170 | else 171 | {out+='
    '+tipuesearch_string_10+'
    ';if(set.minimumLength==1) 172 | {out+='
    '+tipuesearch_string_11+'
    ';} 173 | else 174 | {out+='
    '+tipuesearch_string_12+' '+set.minimumLength+' '+tipuesearch_string_13+'
    ';}}} 175 | $('#tipue_search_content').hide().html(out).slideDown(200);$('#tipue_search_replaced').click(function() 176 | {getTipueSearch(0,false);});$('.tipue_search_related').click(function() 177 | {$('#tipue_search_input').val($(this).attr('id'));getTipueSearch(0,true);});$('.tipue_search_foot_box').click(function() 178 | {var id_v=$(this).attr('id');var id_a=id_v.split('_');getTipueSearch(parseInt(id_a[0]),id_a[1]);});}});};})(jQuery); -------------------------------------------------------------------------------- /aip_site/support/assets/js/search/tipuesearch_set.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Tipue Search 6.1 4 | Copyright (c) 2017 Tipue 5 | Tipue Search is released under the MIT License 6 | http://www.tipue.com/search 7 | */ 8 | 9 | 10 | /* 11 | Stop words 12 | Stop words list from http://www.ranks.nl/stopwords 13 | */ 14 | 15 | var tipuesearch_stop_words = ["a", "about", "above", "after", "again", "against", "all", "am", "an", "and", "any", "are", "aren't", "as", "at", "be", "because", "been", "before", "being", "below", "between", "both", "but", "by", "can't", "cannot", "could", "couldn't", "did", "didn't", "do", "does", "doesn't", "doing", "don't", "down", "during", "each", "few", "for", "from", "further", "had", "hadn't", "has", "hasn't", "have", "haven't", "having", "he", "he'd", "he'll", "he's", "her", "here", "here's", "hers", "herself", "him", "himself", "his", "how", "how's", "i", "i'd", "i'll", "i'm", "i've", "if", "in", "into", "is", "isn't", "it", "it's", "its", "itself", "let's", "me", "more", "most", "mustn't", "my", "myself", "no", "nor", "not", "of", "off", "on", "once", "only", "or", "other", "ought", "our", "ours", "ourselves", "out", "over", "own", "same", "shan't", "she", "she'd", "she'll", "she's", "should", "shouldn't", "so", "some", "such", "than", "that", "that's", "the", "their", "theirs", "them", "themselves", "then", "there", "there's", "these", "they", "they'd", "they'll", "they're", "they've", "this", "those", "through", "to", "too", "under", "until", "up", "very", "was", "wasn't", "we", "we'd", "we'll", "we're", "we've", "were", "weren't", "what", "what's", "when", "when's", "where", "where's", "which", "while", "who", "who's", "whom", "why", "why's", "with", "won't", "would", "wouldn't", "you", "you'd", "you'll", "you're", "you've", "your", "yours", "yourself", "yourselves"]; 16 | 17 | 18 | // Word replace 19 | 20 | var tipuesearch_replace = {'words': [ 21 | {'word': 'tip', 'replace_with': 'tipue'}, 22 | {'word': 'javscript', 'replace_with': 'javascript'}, 23 | {'word': 'jqeury', 'replace_with': 'jquery'} 24 | ]}; 25 | 26 | 27 | // Weighting 28 | 29 | var tipuesearch_weight = {'weight': [ 30 | {'url': 'http://www.tipue.com', 'score': 20}, 31 | {'url': 'http://www.tipue.com/search', 'score': 30}, 32 | {'url': 'http://www.tipue.com/is', 'score': 10} 33 | ]}; 34 | 35 | 36 | // Illogical stemming 37 | 38 | var tipuesearch_stem = {'words': [ 39 | {'word': 'e-mail', 'stem': 'email'}, 40 | {'word': 'javascript', 'stem': 'jquery'}, 41 | {'word': 'javascript', 'stem': 'js'} 42 | ]}; 43 | 44 | 45 | // Related searches 46 | 47 | var tipuesearch_related = {'searches': [ 48 | {'search': 'tipue', 'related': 'Tipue Search'}, 49 | {'search': 'tipue', 'before': 'Tipue Search', 'related': 'Getting Started'}, 50 | {'search': 'tipue', 'before': 'Tipue', 'related': 'jQuery'}, 51 | {'search': 'tipue', 'before': 'Tipue', 'related': 'Blog'} 52 | ]}; 53 | 54 | 55 | // Internal strings 56 | 57 | var tipuesearch_string_1 = 'No title'; 58 | var tipuesearch_string_2 = 'Showing results for'; 59 | var tipuesearch_string_3 = 'Search instead for'; 60 | var tipuesearch_string_4 = '1 result'; 61 | var tipuesearch_string_5 = 'results'; 62 | var tipuesearch_string_6 = 'Back'; 63 | var tipuesearch_string_7 = 'More'; 64 | var tipuesearch_string_8 = 'Nothing found.'; 65 | var tipuesearch_string_9 = 'Common words are largely ignored.'; 66 | var tipuesearch_string_10 = 'Search too short'; 67 | var tipuesearch_string_11 = 'Should be one character or more.'; 68 | var tipuesearch_string_12 = 'Should be'; 69 | var tipuesearch_string_13 = 'characters or more.'; 70 | var tipuesearch_string_14 = 'seconds'; 71 | var tipuesearch_string_15 = 'Searches related to'; 72 | 73 | 74 | // Internals 75 | 76 | 77 | // Timer for showTime 78 | 79 | var startTimer = new Date().getTime(); 80 | 81 | -------------------------------------------------------------------------------- /aip_site/support/assets/js/syntax.js: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Ex post facto improvements to protobuf syntax highlighting. 16 | // This applies special CSS classes to protobuf patterns that are overlooked 17 | // or insufficiently distinguished by the usual Pygments/Rouge syntax parsers. 18 | $.when($.ready).then(() => { 19 | // ------------- 20 | // -- Generic -- 21 | // ------------- 22 | // De-emphasize semicolon punctuation across the board. 23 | $('.highlight .p:contains(;)').each((_, el) => { 24 | el = $(el); 25 | if (el.text() === ';') { 26 | el.addClass('semi'); 27 | } else if (el.text().match(/[;]$/)) { 28 | let txt = el.text(); 29 | el.text(txt.substring(0, txt.length - 1)); 30 | $(';').insertAfter(el); 31 | } 32 | }); 33 | 34 | // -------------- 35 | // -- Protobuf -- 36 | // -------------- 37 | // RPCs are always followed by three names (the RPC name, the request object, 38 | // and the response object) -- designate those appropriately. 39 | $('.language-proto .k:contains(rpc)').each((_, el) => { 40 | $(el).nextAll('.n').slice(1, 3).addClass('nc'); 41 | }); 42 | 43 | // Designate message names as such when using them to delcare fields. 44 | $('.language-proto .n + .na + .o:contains(=)').prev().prev().addClass('nc'); 45 | 46 | // Colons in protocol buffers always come immediately after property keys. 47 | $('.language-proto .n + .o:contains(:)') 48 | .addClass('nk') 49 | .prev() 50 | .addClass('nk'); 51 | 52 | // Split up the beginning punctuation `[(` for field annotations, putting 53 | // them in separate s so we can recolor only the latter. 54 | $('.language-proto .p') 55 | .filter((_, el) => $(el).text() === '[(') 56 | .each((_, el) => { 57 | $(el).text('['); 58 | $('(').insertAfter($(el)); 59 | }); 60 | 61 | // Designate protobuf annotations, which follow a consistent pattern with 62 | // paerentheses. 63 | $('.language-proto .p') 64 | .filter((_, el) => $(el).text() === '(') 65 | .each((_, el) => { 66 | let open = $(el); // ( 67 | let ann = open.next(); // google.api.foo 68 | let close = ann.next(); // ) 69 | 70 | // Sanity check: Does this really look like an annotation? 71 | // 72 | // This checks the existing classes that Pygments uses for proto 73 | // annotations, to ensure we do not get false positive matches 74 | // (since the starting match is just a span with `(` in it). 75 | let conditions = [ 76 | ann.hasClass('n'), 77 | !ann.hasClass('nc'), 78 | close.hasClass('p'), 79 | close.text() === ')', 80 | close.next('.o').text() === '=', 81 | ]; 82 | if (!conditions.every((v) => v === true)) { 83 | return; 84 | } 85 | 86 | // This is an annotation, color it accordingly. 87 | for (let e of [open, ann, close]) { 88 | e.addClass('protobuf-annotation'); 89 | } 90 | }); 91 | 92 | // Highlight constants. 93 | $('.language-proto .o + .n:not(.nc)') 94 | .filter((_, el) => /^[A-Z0-9_]+$/.test($(el).text())) 95 | .each((_, el) => { 96 | $(el).addClass('mi').removeClass('n'); 97 | }); 98 | 99 | // ---------------- 100 | // -- TypeScript -- 101 | // ---------------- 102 | // Interfaces are always followed by what are effectively class names. 103 | for (let lang of ['typescript', 'ts']) { 104 | $(`.language-${lang} .kr:contains(interface)`).each((_, el) => { 105 | if ($(el).nextAll('.nb').length > 0) { 106 | $(el).nextAll('.nb').slice(0, 1).removeClass('nb').addClass('nc'); 107 | } else { 108 | $(el).nextAll('.nx').slice(0, 1).removeClass('nx').addClass('nc'); 109 | } 110 | }); 111 | } 112 | }); 113 | -------------------------------------------------------------------------------- /aip_site/support/assets/misc/ebnf-filtering.txt: -------------------------------------------------------------------------------- 1 | # Filter, possibly empty 2 | filter 3 | : [expression] 4 | ; 5 | 6 | 7 | # Expressions may either be a conjunction (AND) of sequences or a simple 8 | # sequence. 9 | # 10 | # Note, the AND is case-sensitive. 11 | # 12 | # Example: `a b AND c AND d` 13 | # 14 | # The expression `(a b) AND c AND d` is equivalent to the example. 15 | expression 16 | : sequence {WS AND WS sequence} 17 | ; 18 | 19 | # Sequence is composed of one or more whitespace (WS) separated factors. 20 | # 21 | # A sequence expresses a logical relationship between 'factors' where 22 | # the ranking of a filter result may be scored according to the number 23 | # factors that match and other such criteria as the proximity of factors 24 | # to each other within a document. 25 | # 26 | # When filters are used with exact match semantics rather than fuzzy 27 | # match semantics, a sequence is equivalent to AND. 28 | # 29 | # Example: `New York Giants OR Yankees` 30 | # 31 | # The expression `New York (Giants OR Yankees)` is equivalent to the 32 | # example. 33 | sequence 34 | : factor {WS factor} 35 | ; 36 | 37 | 38 | # Factors may either be a disjunction (OR) of terms or a simple term. 39 | # 40 | # Note, the OR is case-sensitive. 41 | # 42 | # Example: `a < 10 OR a >= 100` 43 | factor 44 | : term {WS OR WS term} 45 | ; 46 | 47 | # Terms may either be unary or simple expressions. 48 | # 49 | # Unary expressions negate the simple expression, either mathematically `-` 50 | # or logically `NOT`. The negation styles may be used interchangeably. 51 | # 52 | # Note, the `NOT` is case-sensitive and must be followed by at least one 53 | # whitespace (WS). 54 | # 55 | # Examples: 56 | # * logical not : `NOT (a OR b)` 57 | # * alternative not : `-file:".java"` 58 | # * negation : `-30` 59 | term 60 | : [(NOT WS | MINUS)] simple 61 | ; 62 | 63 | # Simple expressions may either be a restriction or a nested (composite) 64 | # expression. 65 | simple 66 | : restriction 67 | | composite 68 | ; 69 | 70 | # Restrictions express a relationship between a comparable value and a 71 | # single argument. When the restriction only specifies a comparable 72 | # without an operator, this is a global restriction. 73 | # 74 | # Note, restrictions are not whitespace sensitive. 75 | # 76 | # Examples: 77 | # * equality : `package=com.google` 78 | # * inequality : `msg != 'hello'` 79 | # * greater than : `1 > 0` 80 | # * greater or equal : `2.5 >= 2.4` 81 | # * less than : `yesterday < request.time` 82 | # * less or equal : `experiment.rollout <= cohort(request.user)` 83 | # * has : `map:key` 84 | # * global : `prod` 85 | # 86 | # In addition to the global, equality, and ordering operators, filters 87 | # also support the has (`:`) operator. The has operator is unique in 88 | # that it can test for presence or value based on the proto3 type of 89 | # the `comparable` value. The has operator is useful for validating the 90 | # structure and contents of complex values. 91 | restriction 92 | : comparable [comparator arg] 93 | ; 94 | 95 | # Comparable may either be a member or function. 96 | comparable 97 | : member 98 | | function 99 | ; 100 | 101 | # Member expressions are either value or DOT qualified field references. 102 | # 103 | # Example: `expr.type_map.1.type` 104 | member 105 | : value {DOT field} 106 | ; 107 | 108 | # Function calls may use simple or qualified names with zero or more 109 | # arguments. 110 | # 111 | # All functions declared within the list filter, apart from the special 112 | # `arguments` function must be provided by the host service. 113 | # 114 | # Examples: 115 | # * `regex(m.key, '^.*prod.*$')` 116 | # * `math.mem('30mb')` 117 | # 118 | # Antipattern: simple and qualified function names may include keywords: 119 | # NOT, AND, OR. It is not recommended that any of these names be used 120 | # within functions exposed by a service that supports list filters. 121 | function 122 | : name {DOT name} LPAREN [argList] RPAREN 123 | ; 124 | 125 | # Comparators supported by list filters. 126 | comparator 127 | : LESS_EQUALS # <= 128 | | LESS_THAN # < 129 | | GREATER_EQUALS # >= 130 | | GREATER_THAN # > 131 | | NOT_EQUALS # != 132 | | EQUALS # = 133 | | HAS # : 134 | ; 135 | 136 | # Composite is a parenthesized expression, commonly used to group 137 | # terms or clarify operator precedence. 138 | # 139 | # Example: `(msg.endsWith('world') AND retries < 10)` 140 | composite 141 | : LPAREN expression RPAREN 142 | ; 143 | 144 | # Value may either be a TEXT or STRING. 145 | # 146 | # TEXT is a free-form set of characters without whitespace (WS) 147 | # or . (DOT) within it. The text may represent a variable, string, 148 | # number, boolean, or alternative literal value and must be handled 149 | # in a manner consistent with the service's intention. 150 | # 151 | # STRING is a quoted string which may or may not contain a special 152 | # wildcard `*` character at the beginning or end of the string to 153 | # indicate a prefix or suffix-based search within a restriction. 154 | value 155 | : TEXT 156 | | STRING 157 | ; 158 | 159 | # Fields may be either a value or a keyword. 160 | field 161 | : value 162 | | keyword 163 | ; 164 | 165 | # Names may either be TEXT or a keyword. 166 | name 167 | : TEXT 168 | | keyword 169 | ; 170 | 171 | argList 172 | : arg { COMMA arg} 173 | ; 174 | 175 | arg 176 | : comparable 177 | | composite 178 | ; 179 | 180 | keyword 181 | : NOT 182 | | AND 183 | | OR 184 | ; 185 | -------------------------------------------------------------------------------- /aip_site/support/scss/hero.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | .aip-hero { 4 | .aip-hero-banner { 5 | margin-top: 3em; 6 | border-bottom: 1px solid $glue-grey-300; 7 | 8 | .aip-hero-title { 9 | font-size: 250%; 10 | font-weight: bold; 11 | } 12 | .aip-hero-subtitle { 13 | font-size: 125%; 14 | margin-top: 1em; 15 | } 16 | 17 | .aip-buttons { 18 | padding-top: 2em; 19 | padding-bottom: 2em; 20 | 21 | > a { 22 | margin-right: 1em; 23 | } 24 | } 25 | } 26 | 27 | .aip-landing-content { 28 | padding-top: 1em; 29 | padding-bottom: 1em; 30 | border-bottom: 1px solid $glue-grey-300; 31 | 32 | h1 { 33 | font-size: 160%; 34 | font-weight: bold; 35 | } 36 | p { 37 | font-size: 115%; 38 | } 39 | } 40 | 41 | .aip-shortcuts { 42 | padding-top: 1em; 43 | 44 | .aip-shortcut { 45 | padding-top: 1em; 46 | 47 | > h1 { 48 | font-weight: bold; 49 | font-size: 120%; 50 | } 51 | 52 | > a { 53 | margin-top: 1em; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/aip/badges.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | .aip-state { 4 | border-radius: 4px; 5 | font-size: 0.75em; 6 | margin-left: 10px; 7 | padding: 2px 5px; 8 | } 9 | 10 | .aip-state-reviewing { 11 | background-color: $glue-blue-50; 12 | border: 1px solid $glue-blue-200; 13 | } 14 | 15 | .aip-state-draft { 16 | background-color: $glue-yellow-50; 17 | border: 1px solid $glue-yellow-200; 18 | } 19 | 20 | h3.reviewing::after { 21 | @extend .aip-state; 22 | @extend .aip-state-reviewing; 23 | content: 'Reviewing'; 24 | font-size: 0.5em; 25 | } 26 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/aip/breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | ol.aip-breadcrumbs { 4 | margin-bottom: 0; 5 | 6 | .glue-breadcrumbs__item:not(:last-child)::after { 7 | color: $glue-grey-700; 8 | content: '»'; 9 | margin-left: 3px; 10 | margin-right: 1px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/aip/header.scss: -------------------------------------------------------------------------------- 1 | .h-c-header__cta-li form.h-c-sitesearch { 2 | padding-right: 20px; 3 | } 4 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/aip/nav.scss: -------------------------------------------------------------------------------- 1 | #aip-nav { 2 | .nav-item { 3 | span.aip-number { 4 | display: inline-block; 5 | min-width: 26px; 6 | text-align: right; 7 | 8 | &:after { 9 | content: '. '; 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/aip/news.scss: -------------------------------------------------------------------------------- 1 | .aip-newsletter-highlights { 2 | > p { 3 | font-size: 0.85em; 4 | margin-top: 0.5em; 5 | } 6 | 7 | ul.highlights-list { 8 | margin-top: 0px; 9 | margin-left: 0px; 10 | padding-left: 10px; 11 | list-style: none; 12 | 13 | > li { 14 | font-size: 0.85em; 15 | &:before { 16 | content: '- '; 17 | } 18 | } 19 | 20 | > li > ul { 21 | display: inline; 22 | margin-left: 0px; 23 | padding-left: 0px; 24 | 25 | > li { 26 | display: inline; 27 | 28 | a { 29 | text-decoration: none; 30 | } 31 | 32 | &:after { 33 | content: ', '; 34 | } 35 | &:last-child:after { 36 | content: ''; 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/aip/tables.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | #aip-main { 4 | table { 5 | &:not(.no-h) { 6 | &.aip-listing { 7 | margin-top: 10px; 8 | 9 | tr:first-child { 10 | border-top: 2px solid $glue-grey-500; 11 | } 12 | 13 | th { 14 | display: none; 15 | } 16 | 17 | td { 18 | text-align: left; 19 | 20 | // The Number column. 21 | &:first-child { 22 | text-align: right; 23 | width: 70px; 24 | } 25 | 26 | a { 27 | text-decoration: none; 28 | } 29 | } 30 | 31 | tr td { 32 | padding-top: 10px; 33 | padding-bottom: 10px; 34 | } 35 | } 36 | } 37 | } 38 | 39 | table#aip-summary { 40 | float: right; 41 | } 42 | } 43 | 44 | table#aip-summary { 45 | background-color: $glue-grey-50; 46 | border: 1px solid $glue-grey-200; 47 | margin-top: 2px; 48 | margin-left: 10px; 49 | width: 220px; 50 | 51 | @media (max-width: 800px) { 52 | display: none; 53 | } 54 | 55 | th { 56 | border: 0px; 57 | color: $glue-grey-800; 58 | font-size: 14px; 59 | padding: 8px 0px; 60 | text-align: center; 61 | } 62 | 63 | tr { 64 | td { 65 | font-size: 12px; 66 | padding: 2px 5px; 67 | 68 | &:first-child { 69 | color: $glue-grey-700; 70 | font-weight: bold; 71 | padding-left: 12px; 72 | width: 60px; 73 | } 74 | 75 | a { 76 | text-decoration: none; 77 | } 78 | } 79 | 80 | &:last-child td { 81 | padding-bottom: 8px; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/callouts.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | .callout { 4 | margin-top: 10px; 5 | margin-left: 20px; 6 | margin-right: 20px; 7 | padding: 10px 20px; 8 | color: $glue-grey-800; 9 | } 10 | 11 | .callout + .callout { 12 | margin-top: 15px; 13 | } 14 | 15 | .important { 16 | @extend .callout; 17 | background-color: $glue-yellow-50; 18 | border: 1px solid $glue-yellow-200; 19 | } 20 | 21 | .note { 22 | @extend .callout; 23 | background-color: $glue-blue-50; 24 | border: 1px solid $glue-blue-200; 25 | } 26 | 27 | .tldr, 28 | .summary { 29 | @extend .callout; 30 | background-color: $glue-green-50; 31 | border: 1px solid $glue-green-200; 32 | } 33 | 34 | .warning { 35 | @extend .callout; 36 | background-color: $glue-red-50; 37 | border: 1px solid $glue-red-200; 38 | } 39 | 40 | code { 41 | font-size: 90% !important; 42 | } 43 | 44 | .docs-component-main { 45 | blockquote { 46 | border-left: 4px solid $glue-grey-500; 47 | margin: 20px; 48 | padding-left: 20px; 49 | } 50 | 51 | code { 52 | font-size: 0.9em; 53 | } 54 | 55 | pre { 56 | font-size: 14px; 57 | margin: 20px; 58 | padding: 9px 14px; 59 | 60 | code { 61 | color: $glue-grey-900; 62 | } 63 | } 64 | } 65 | 66 | .digraph { 67 | text-align: center; 68 | } 69 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/colors.scss: -------------------------------------------------------------------------------- 1 | // This is forked. Do not edit. 2 | $glue-greys: ( 3 | 0: #fff, 4 | 25: #f1f1f1, 5 | 50: #f8f9fa, 6 | 100: #f1f3f4, 7 | 200: #e8eaed, 8 | 300: #dadce0, 9 | 400: #bdc1c6, 10 | 500: #9aa0a6, 11 | 600: #80868b, 12 | 700: #5f6368, 13 | 800: #3c4043, 14 | 900: #202124, 15 | ); 16 | $glue-blues: ( 17 | 50: #e8f0fe, 18 | 100: #d2e3fc, 19 | 200: #aecbfa, 20 | 300: #8ab4f8, 21 | 400: #669df6, 22 | 500: #4285f4, 23 | 600: #1a73e8, 24 | 700: #1967d2, 25 | 800: #185abc, 26 | 900: #174ea6, 27 | ); 28 | $glue-greens: ( 29 | 50: #e6f4ea, 30 | 100: #ceead6, 31 | 200: #a8dab5, 32 | 300: #81c995, 33 | 400: #5bb974, 34 | 500: #34a853, 35 | 600: #1e8e3e, 36 | 700: #188038, 37 | 800: #137333, 38 | 900: #0d652d, 39 | ); 40 | $glue-reds: ( 41 | 50: #fce8e6, 42 | 100: #fad2cf, 43 | 200: #f6aea9, 44 | 300: #f28b82, 45 | 400: #ee675c, 46 | 500: #ea4335, 47 | 600: #d93025, 48 | 700: #c5221f, 49 | 800: #b31412, 50 | 900: #a50e0e, 51 | ); 52 | $glue-yellows: ( 53 | 50: #fef7e0, 54 | 100: #feefc3, 55 | 200: #fde293, 56 | 300: #fdd663, 57 | 400: #fcc934, 58 | 500: #fbbc04, 59 | 600: #f9ab00, 60 | 700: #f29900, 61 | 800: #ea8600, 62 | 900: #e37400, 63 | ); 64 | $glue-purples: ( 65 | 900: #681da8, 66 | ); 67 | // mws color spec 2.0 68 | $glue-grey-0: map-get($glue-greys, 0); 69 | $glue-grey-25: map-get($glue-greys, 25); 70 | $glue-grey-50: map-get($glue-greys, 50); 71 | $glue-grey-100: map-get($glue-greys, 100); 72 | $glue-grey-200: map-get($glue-greys, 200); 73 | $glue-grey-300: map-get($glue-greys, 300); 74 | $glue-grey-400: map-get($glue-greys, 400); 75 | $glue-grey-500: map-get($glue-greys, 500); 76 | $glue-grey-600: map-get($glue-greys, 600); 77 | $glue-grey-700: map-get($glue-greys, 700); 78 | $glue-grey-800: map-get($glue-greys, 800); 79 | $glue-grey-900: map-get($glue-greys, 900); 80 | $glue-blue-50: map-get($glue-blues, 50); 81 | $glue-blue-100: map-get($glue-blues, 100); 82 | $glue-blue-200: map-get($glue-blues, 200); 83 | $glue-blue-300: map-get($glue-blues, 300); 84 | $glue-blue-400: map-get($glue-blues, 400); 85 | $glue-blue-500: map-get($glue-blues, 500); 86 | $glue-blue-600: map-get($glue-blues, 600); 87 | $glue-blue-700: map-get($glue-blues, 700); 88 | $glue-blue-800: map-get($glue-blues, 800); 89 | $glue-blue-900: map-get($glue-blues, 900); 90 | $glue-green-50: map-get($glue-greens, 50); 91 | $glue-green-100: map-get($glue-greens, 100); 92 | $glue-green-200: map-get($glue-greens, 200); 93 | $glue-green-300: map-get($glue-greens, 300); 94 | $glue-green-400: map-get($glue-greens, 400); 95 | $glue-green-500: map-get($glue-greens, 500); 96 | $glue-green-600: map-get($glue-greens, 600); 97 | $glue-green-700: map-get($glue-greens, 700); 98 | $glue-green-800: map-get($glue-greens, 800); 99 | $glue-green-900: map-get($glue-greens, 900); 100 | $glue-red-50: map-get($glue-reds, 50); 101 | $glue-red-100: map-get($glue-reds, 100); 102 | $glue-red-200: map-get($glue-reds, 200); 103 | $glue-red-300: map-get($glue-reds, 300); 104 | $glue-red-400: map-get($glue-reds, 400); 105 | $glue-red-500: map-get($glue-reds, 500); 106 | $glue-red-600: map-get($glue-reds, 600); 107 | $glue-red-700: map-get($glue-reds, 700); 108 | $glue-red-800: map-get($glue-reds, 800); 109 | $glue-red-900: map-get($glue-reds, 900); 110 | $glue-yellow-50: map-get($glue-yellows, 50); 111 | $glue-yellow-100: map-get($glue-yellows, 100); 112 | $glue-yellow-200: map-get($glue-yellows, 200); 113 | $glue-yellow-300: map-get($glue-yellows, 300); 114 | $glue-yellow-400: map-get($glue-yellows, 400); 115 | $glue-yellow-500: map-get($glue-yellows, 500); 116 | $glue-yellow-600: map-get($glue-yellows, 600); 117 | $glue-yellow-700: map-get($glue-yellows, 700); 118 | $glue-yellow-800: map-get($glue-yellows, 800); 119 | $glue-yellow-900: map-get($glue-yellows, 900); 120 | // extended color palette 121 | $glue-purple-900: map-get($glue-purples, 900); 122 | $glue-color-name-to-palette: ( 123 | 'grey': $glue-greys, 124 | 'blue': $glue-blues, 125 | 'green': $glue-greens, 126 | 'red': $glue-reds, 127 | 'yellow': $glue-yellows, 128 | 'purple': $glue-purples, 129 | ); 130 | // Generic color variables - common colors for components. 131 | $glue-color-primary-black: $glue-grey-900 !default; 132 | $glue-color-border: $glue-grey-300 !default; 133 | $glue-color-focus-background: $glue-grey-100 !default; 134 | $glue-color-link-resting: $glue-blue-600 !default; 135 | $glue-color-link-active: $glue-blue-900 !default; 136 | $glue-color-link-visited: $glue-purple-900 !default; 137 | $glue-color-focus-background-rgba: rgba($glue-grey-900, 0.06) !default; 138 | $glue-white: $glue-grey-0 !default; 139 | $glue-black: #000 !default; 140 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/footer.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | footer { 4 | border-top: 1px solid $glue-grey-300; 5 | color: $glue-grey-700; 6 | font-size: 13px; 7 | font-style: italic; 8 | margin-bottom: 10px; 9 | margin-top: 25px; 10 | padding-top: 10px; 11 | 12 | p { 13 | font-size: 13px; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/header.scss: -------------------------------------------------------------------------------- 1 | @media (max-width: 1200px) { 2 | .glue-header .h-c-header__bar .h-c-header__hamburger:first-child { 3 | display: table !important; 4 | } 5 | } 6 | 7 | .github-logo { 8 | width: 1em; 9 | height: 1em; 10 | margin-right: 10px; 11 | margin-top: -2px; 12 | } 13 | 14 | .glue-button img.github-logo { 15 | display: inline; 16 | } 17 | 18 | body .glue-header.glue-header--single .glue-header__bar { 19 | box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.3), 20 | 0 1px 3px 1px rgba(60, 64, 67, 0.15); 21 | 22 | @media (min-width: 1024px) { 23 | transform: none !important; 24 | 25 | @media (max-width: 1096px) { 26 | .glue-header__item.header-item-collapse { 27 | display: none; 28 | } 29 | } 30 | } 31 | 32 | @media (max-width: 599px) { 33 | &.glue-header__bar--mobile { 34 | height: 64px; 35 | min-height: 64px; 36 | 37 | .glue-header__cta { 38 | display: none; 39 | } 40 | } 41 | } 42 | } 43 | 44 | .glue-sitesearch { 45 | label { 46 | display: none; 47 | } 48 | 49 | @media (max-width: 1023px) { 50 | display: none; 51 | } 52 | 53 | @media (max-width: 1200px) { 54 | .glue-sitesearch__input { 55 | width: 150px; 56 | } 57 | } 58 | 59 | .glue-button.glue-sitesearch__submit { 60 | background: transparent; 61 | border: 0px; 62 | border-radius: 0%; 63 | min-width: 36px; 64 | 65 | svg { 66 | display: block; 67 | max-width: 24px; 68 | width: 24px; 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/headings.scss: -------------------------------------------------------------------------------- 1 | // Headers 2 | .docs-component-main { 3 | h1, 4 | h2, 5 | h3, 6 | h4, 7 | h5, 8 | a[name] { 9 | // This creates a "fake block" above the header that does not show up 10 | // anywhere but tricks the browser into thinking that the anchor is 80px 11 | // higher than it actually is. 12 | cursor: default; 13 | outline: none; 14 | 15 | &::before { 16 | display: block; 17 | content: ' '; 18 | height: 80px; 19 | margin-top: -80px; 20 | pointer-events: none; 21 | visibility: hidden; 22 | } 23 | 24 | a { 25 | text-decoration: none; 26 | } 27 | } 28 | h4 { 29 | padding-bottom: 5px; 30 | 31 | &.aip-number { 32 | line-height: 1em; 33 | 34 | & + h1 { 35 | margin-top: 0px; 36 | margin-left: -2px; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/lists.scss: -------------------------------------------------------------------------------- 1 | .docs-component-main { 2 | ul { 3 | margin-top: 20px; 4 | padding-left: 20px; 5 | 6 | > li > ul { 7 | margin-top: 0px; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/nav.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | main#page-content { 4 | margin-top: 16px; 5 | } 6 | 7 | nav.docs-component-nav { 8 | display: none; 9 | } 10 | 11 | nav li.drawer-heading { 12 | padding-left: 16px; 13 | padding-right: 16px; 14 | 15 | &:not(:first-child) { 16 | margin-top: 32px; 17 | } 18 | 19 | .drawer-heading-text { 20 | color: $glue-grey-600; 21 | display: table-cell; 22 | font-family: 'Google Sans', 'Roboto', Arial, Helvetica, sans-serif; 23 | font-size: 14px; 24 | font-weight: 700; 25 | height: 48px; 26 | letter-spacing: 0.25px; 27 | vertical-align: middle; 28 | } 29 | } 30 | 31 | // This is a hack. 32 | // For some reason, the entire
    element is positioned incorrectly 33 | // (underneath the top bar) if the breadcrumb is not present. 34 | // We have breadcrumbs on everything except the front page, and so the left 35 | // nav was tucking under the header bar on the front page. 36 | // 37 | // I can not figure out what makes the inclusion of the breadcrumb the 38 | // silver bullet here (in fact, there has to be a list element inside the 39 | // breadcrumb, also), so instead I am giving up and hiding it. 40 | nav.no-breadcrumb { 41 | height: 0px; 42 | visibility: hidden; 43 | 44 | & + h1 { 45 | margin-top: -88px; // Normally -80. 46 | } 47 | } 48 | 49 | @media (min-width: 1200px) { 50 | .docs-component-main { 51 | padding-left: 320px; 52 | } 53 | 54 | nav.docs-component-nav { 55 | background-color: $glue-grey-100; 56 | display: block; 57 | float: left; 58 | margin-right: 40px; 59 | overflow-x: hidden; 60 | overflow-y: auto; 61 | padding: 16px 12px; 62 | position: fixed; 63 | width: 280px; 64 | 65 | &::-webkit-scrollbar { 66 | height: 4px; 67 | width: 4px; 68 | } 69 | 70 | &::-webkit-scrollbar-thumb { 71 | background: rgba(0, 0, 0, 0.25); 72 | } 73 | 74 | ul.nav-list { 75 | margin-left: 0px; 76 | margin-bottom: 0px; 77 | 78 | li.nav-item { 79 | font-size: 13px; 80 | font-weight: 400; 81 | line-height: 16px; 82 | list-style: none; 83 | margin-top: 8px; 84 | overflow-x: hidden; 85 | overflow-y: hidden; 86 | text-overflow: ellipsis; 87 | 88 | &:first-child { 89 | margin-top: 0px; 90 | } 91 | 92 | a { 93 | color: $glue-grey-900; 94 | text-decoration: none; 95 | 96 | &:hover { 97 | color: $glue-blue-600; 98 | } 99 | } 100 | 101 | svg { 102 | color: $glue-grey-500; 103 | height: 1.25em; 104 | vertical-align: middle; 105 | width: 1.25em; 106 | } 107 | 108 | &.nav-item-active a { 109 | color: $glue-blue-600; 110 | } 111 | 112 | &.nav-item-header { 113 | color: $glue-grey-600; 114 | font-size: 14px; 115 | font-weight: 500; 116 | 117 | &:not(:first-child) { 118 | border-top: 1px solid #cfd8dc; 119 | margin: 15px -16px 0px -16px; 120 | padding: 15px 16px 0px 16px; 121 | } 122 | } 123 | } 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/sidebar.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | // TOC Sidebar 4 | .docs-component-sidebar { 5 | display: none; 6 | } 7 | 8 | @media (min-width: 1024px) { 9 | .docs-component-main { 10 | padding-right: 250px; 11 | } 12 | 13 | .docs-component-sidebar { 14 | -webkit-position: sticky; 15 | background: #fff; 16 | display: block; 17 | float: right; 18 | position: sticky; 19 | right: 20px; 20 | top: 80px; 21 | width: 230px; 22 | 23 | .docs-component-sidebar-toc > .toc { 24 | border-left: 3px solid $glue-blue-700; 25 | margin-left: 20px; 26 | padding-left: 20px; 27 | 28 | > ul { 29 | margin-left: 0px; 30 | } 31 | 32 | .toctitle { 33 | font-weight: 700; 34 | text-transform: uppercase; 35 | 36 | &:only-child { 37 | display: none; 38 | } 39 | } 40 | 41 | li { 42 | font-size: 14px; 43 | list-style: none; 44 | margin-top: 12px; 45 | 46 | a { 47 | color: $glue-grey-900; 48 | font-weight: 300; 49 | text-decoration: none; 50 | } 51 | } 52 | } 53 | 54 | .docs-component-sidebar-actions > ul li { 55 | font-weight: 700; 56 | font-size: 16px; 57 | list-style: none; 58 | margin-top: 12px; 59 | text-transform: uppercase; 60 | 61 | a, 62 | a:visited { 63 | color: $glue-blue-700; 64 | text-decoration: none; 65 | } 66 | 67 | a:hover, 68 | a:active, 69 | a:focus { 70 | color: $glue-blue-500; 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/syntax.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | strong.spec-directive { 4 | &.spec-must { 5 | color: $glue-red-700; 6 | } 7 | 8 | &.spec-should { 9 | color: #f57c00; 10 | } 11 | 12 | &.spec-may { 13 | color: $glue-green-700; 14 | } 15 | } 16 | 17 | code, 18 | .highlighter-rouge { 19 | background: transparent; 20 | color: inherit; 21 | padding: 0px; 22 | } 23 | 24 | .highlight { 25 | .hll { 26 | background-color: #ffffcc; 27 | } 28 | 29 | // -------------------------------------------- 30 | // Common things (comments, keywords, literals) 31 | // -------------------------------------------- 32 | 33 | // Built-in: Pseudo (.bp) 34 | .bp { 35 | color: #03a9f4; 36 | } 37 | 38 | // Comment (.c) 39 | // Single-line comment (.c1) 40 | // Multi-line comment (.cm) 41 | .c, 42 | .c1, 43 | .cm { 44 | color: $glue-green-700; 45 | font-style: italic; 46 | } 47 | 48 | // Keyword (.k) 49 | // Keyword: Constant (.kc) 50 | // Keyword: Declaration (.kd) 51 | // Keyword: Namespace (.kn) 52 | // Keyword: Pseudo (.kp) 53 | // Keyword: Reserved (.kr) 54 | .k, 55 | .kc, 56 | .kd, 57 | .kn, 58 | .kp, 59 | .kr { 60 | color: $glue-blue-900; 61 | font-weight: bold; 62 | } 63 | 64 | // Keyword: Type (.kt) 65 | .kt { 66 | color: #ef6c00; 67 | } 68 | 69 | // Name: Attribute 70 | .na { 71 | color: #ad1457; 72 | } 73 | 74 | // Name: Built-in (.nb) 75 | .nb { 76 | @extend .kt; 77 | } 78 | 79 | // Name: Class (.nc) 80 | .nc { 81 | color: #fb8c00; 82 | } 83 | 84 | // Name: Key (.nk) 85 | // Name: Other (.nx) 86 | .nk, 87 | .nx { 88 | color: #6a1b9a; 89 | } 90 | 91 | // Literal: Number (.m) 92 | // Literal: Float (.mf) 93 | // Literal: Hex (.mh) 94 | // Literal: Integer (.mi) 95 | // Literal: Octal (.mo) 96 | // Literal: Long (.il) 97 | .m, 98 | .mf, 99 | .mh, 100 | .mi, 101 | .mo, 102 | .il { 103 | color: #03a9f4; 104 | } 105 | 106 | // String (.s) 107 | // Backtick string (.sb) 108 | // Docstring (.sd) 109 | // Double-quoted string (.s2) 110 | // Heredoc (.sh) 111 | // Single-quoted string (.s1) 112 | // String interpolation (.si) 113 | // String: Other (.sx) 114 | .s, 115 | .sb, 116 | .sd, 117 | .sh, 118 | .si, 119 | .sx, 120 | .s1, 121 | .s2 { 122 | color: $glue-blue-500; 123 | } 124 | 125 | // Operator: Symbol 126 | .o { 127 | font-weight: bold; 128 | } 129 | 130 | // Operator: Word 131 | .ow { 132 | @extend .k; 133 | } 134 | 135 | // Punctuation: Semicolons 136 | .p.semi { 137 | color: $glue-grey-400; 138 | } 139 | 140 | // -------- 141 | // Generics 142 | // -------- 143 | 144 | // Generic: Deleted 145 | .gd { 146 | color: black; 147 | background-color: $glue-red-50; 148 | } 149 | 150 | // Generic: Emphasis 151 | .ge { 152 | font-style: italic; 153 | } 154 | 155 | // Generic: Error (.gr) 156 | // Generic: Traceback (.gt) 157 | .gr, 158 | .gt { 159 | color: $glue-red-900; 160 | } 161 | 162 | // Generic: Heading 163 | .gh { 164 | color: $glue-grey-500; 165 | } 166 | 167 | // Generic: Inserted 168 | .gi { 169 | color: black; 170 | background-color: $glue-green-50; 171 | } 172 | 173 | // Generic: Output 174 | .go { 175 | color: $glue-grey-600; 176 | } 177 | 178 | // Generic: Prompt 179 | .gp { 180 | color: $glue-grey-800; 181 | } 182 | 183 | // Generic: Strong 184 | .gs { 185 | font-weight: bold; 186 | } 187 | 188 | // Generic: Subheading 189 | .gu { 190 | color: $glue-grey-400; 191 | } 192 | 193 | // --------------- 194 | // Uncommon things 195 | // --------------- 196 | 197 | // Preprocessor comment 198 | .cp { 199 | color: #008080; 200 | } 201 | 202 | // Special comment 203 | .cs { 204 | @extend .c; 205 | font-style: normal; 206 | font-weight: bold; 207 | } 208 | 209 | // Error 210 | .err { 211 | color: $glue-red-700; 212 | background-color: $glue-red-100; 213 | } 214 | 215 | // Name: Tag 216 | .nt { 217 | @extend .k; 218 | } 219 | 220 | // Text: Whitespace 221 | .w { 222 | color: $glue-grey-400; 223 | } 224 | 225 | // Literal: Char 226 | .sc { 227 | @extend .m; 228 | } 229 | 230 | // String: Escape sequence 231 | .se { 232 | color: #03a9f4; 233 | } 234 | 235 | // String: Regex 236 | .sr { 237 | color: $glue-blue-600; 238 | } 239 | 240 | // Literal: Symbol 241 | .ss { 242 | @extend .m; 243 | } 244 | 245 | // ----------------- 246 | // Language-specific 247 | // ----------------- 248 | .protobuf-annotation { 249 | color: $glue-yellow-600; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/tables.scss: -------------------------------------------------------------------------------- 1 | // Tables 2 | table:not(.no-h) { 3 | margin-bottom: 20px !important; 4 | 5 | tr td { 6 | font-weight: 300; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /aip_site/support/scss/imports/tabs.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | .tabbed-set { 4 | display: flex; 5 | position: relative; 6 | flex-wrap: wrap; 7 | 8 | .tabbed-content { 9 | display: none; 10 | order: 99; 11 | width: 100%; 12 | } 13 | 14 | label { 15 | border-radius: 10px 10px 0px 0px; 16 | color: $glue-grey-600; 17 | cursor: pointer; 18 | font-family: 'Google Sans', 'Roboto', Arial, Helvetica, sans-serif; 19 | font-size: 80%; 20 | font-weight: 500; 21 | margin: 0 0.5em; 22 | padding: 1.25em; 23 | width: auto; 24 | } 25 | 26 | input { 27 | position: absolute; 28 | opacity: 0; 29 | 30 | &:nth-child(n + 1) { 31 | color: $glue-grey-900; 32 | } 33 | 34 | &:checked + label { 35 | background-color: $glue-grey-100; 36 | border-bottom: 2px solid $glue-blue-700; 37 | color: $glue-grey-900; 38 | z-index: 1; 39 | 40 | + .tabbed-content { 41 | border-top: 2px solid $glue-grey-100; 42 | display: block; 43 | margin-top: -2px; 44 | padding-top: 1em; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /aip_site/support/scss/print.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors.scss'; 2 | @import 'imports/footer.scss'; 3 | 4 | .aip-breadcrumbs, 5 | #aip-nav, 6 | #aip-nav-mobile, 7 | #aip-sidebar, 8 | .glue-header, 9 | .glue-page svg:first-child, 10 | .no-print { 11 | display: none; 12 | } 13 | 14 | h1, 15 | h2, 16 | h3, 17 | h4 { 18 | font-family: 'Google Sans', 'Roboto', Arial, Helvetica, sans-serif; 19 | font-weight: 400; 20 | margin: 0.5em 0em 0em 0em; 21 | padding: 0px; 22 | } 23 | 24 | h1 { 25 | font-size: 2.5em; 26 | } 27 | 28 | h4 + h1 { 29 | margin-top: 0in; 30 | } 31 | 32 | html { 33 | font-family: sans-serif; 34 | } 35 | 36 | kbd, 37 | pre, 38 | samp { 39 | font-family: monospace, monospace; 40 | font-size: 1em; 41 | } 42 | 43 | body { 44 | font-family: 'Roboto', Arial, Helvetica, sans-serif; 45 | font-size: 1em; 46 | font-style: normal; 47 | font-weight: 300; 48 | line-height: 1.444; 49 | } 50 | 51 | code { 52 | font-family: 'Consolas', 'Monaco', 'Roboto Mono', 'Bitstream Vera Sans Mono', 53 | 'Courier New', Courier, monospace; 54 | font-weight: 300; 55 | } 56 | 57 | div.highlighter-rouge { 58 | background: $glue-grey-100; 59 | border: 1px solid $glue-grey-300; 60 | margin-left: 0.5in; 61 | margin-right: 0.5in; 62 | padding-left: 0.2in; 63 | padding-right: 0.2in; 64 | } 65 | 66 | a { 67 | color: inherit; 68 | text-decoration: none; 69 | } 70 | -------------------------------------------------------------------------------- /aip_site/support/scss/search.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/colors'; 2 | 3 | .docs-component-sidebar-toc { 4 | display: none; 5 | } 6 | 7 | .primary-search { 8 | margin-top: 20px; 9 | 10 | input { 11 | font-size: 24px; 12 | height: 48px; 13 | padding-left: 8px; 14 | 15 | @media (min-width: 1024px) { 16 | margin-right: 10px; 17 | width: 500px; 18 | } 19 | } 20 | 21 | button.glue-button { 22 | margin-top: 1px; 23 | } 24 | } 25 | 26 | #tipue_search_content { 27 | margin-left: 2px; 28 | 29 | #tipue_search_results_count { 30 | color: $glue-grey-700; 31 | font-size: 13px; 32 | font-style: italic; 33 | } 34 | 35 | .tipue_search_content_title a { 36 | text-decoration: none; 37 | } 38 | 39 | .tipue_search_content_url { 40 | font-size: 75%; 41 | a { 42 | color: $glue-green-400; 43 | text-decoration: none; 44 | } 45 | } 46 | 47 | .tipue_search_content_text { 48 | font-size: 85%; 49 | margin-bottom: 1em; 50 | } 51 | 52 | .tipue_search_content_bold { 53 | font-weight: bold; 54 | } 55 | } 56 | 57 | #tipue_search_foot_boxes { 58 | li { 59 | border: 1px solid $glue-grey-700; 60 | color: $glue-grey-900; 61 | display: inline-block; 62 | list-style: none; 63 | margin-left: -1px; 64 | min-height: 40px; 65 | min-width: 40px; 66 | padding: 8px 12px; 67 | position: relative; 68 | text-align: center; 69 | 70 | &.current { 71 | background-color: $glue-grey-300; 72 | } 73 | 74 | &:not(.current) { 75 | cursor: pointer; 76 | 77 | &:hover { 78 | background-color: $glue-blue-100; 79 | } 80 | } 81 | 82 | a { 83 | color: $glue-grey-900; 84 | text-decoration: none; 85 | 86 | &:hover { 87 | background-color: transparent; 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /aip_site/support/scss/style.scss: -------------------------------------------------------------------------------- 1 | @import 'imports/callouts'; 2 | @import 'imports/colors'; 3 | @import 'imports/footer'; 4 | @import 'imports/header'; 5 | @import 'imports/headings'; 6 | @import 'imports/lists'; 7 | @import 'imports/nav'; 8 | @import 'imports/sidebar'; 9 | @import 'imports/syntax'; 10 | @import 'imports/tables'; 11 | @import 'imports/tabs'; 12 | 13 | @import 'imports/aip/badges'; 14 | @import 'imports/aip/breadcrumbs'; 15 | @import 'imports/aip/header'; 16 | @import 'imports/aip/nav'; 17 | @import 'imports/aip/news'; 18 | @import 'imports/aip/tables'; 19 | -------------------------------------------------------------------------------- /aip_site/support/templates/aip-listing.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html.j2' %} 2 | 3 | {% block title %}{{ scope.title }} AIPs{% endblock %} 4 | 5 | {% block nav %} 6 | {{ nav.render_nav(site, path, scope_code=scope.code) -}} 7 | {% endblock %} 8 | 9 | {% block content %} 10 | 30 | 31 |
    32 | {% with title = '{0} AIPs'.format(scope.title) -%} 33 | {% include 'includes/breadcrumb.html.j2' %} 34 | {% endwith -%} 35 | 36 | {% for cat in scope.categories.values() -%} 37 | {% if cat.aips -%} 38 |

    {{ cat.title }}

    39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {% for aip in cat.aips.values() -%} 48 | {% if aip.state in ('approved', 'reviewing', 'draft') -%} 49 | 50 | 51 | 59 | 60 | {% endif -%} 61 | {% endfor -%} 62 | 63 |
    NumberTitle
    {{ aip.id }} 52 | {{ aip.title }} 53 | {% if aip.state != 'approved' -%} 54 | 55 | {{ aip.state | capitalize }} 56 | 57 | {% endif -%} 58 |
    64 | {% endif -%} 65 | {% endfor -%} 66 | 67 | {% include 'includes/footer.html.j2' %} 68 |
    69 | {% endblock %} 70 | -------------------------------------------------------------------------------- /aip_site/support/templates/aip.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html.j2' %} 2 | 3 | {% block title -%} 4 | AIP-{{ aip.id }}: {{ aip.title }} 5 | {%- endblock %} 6 | 7 | {% block js %} 8 | 9 | {% if 'js_scripts' in aip.config -%} 10 | {% for js_script in aip.config.js_scripts -%} 11 | 12 | {% endfor -%} 13 | {% endif -%} 14 | {% endblock %} 15 | 16 | {% block nav %} 17 | {{ nav.render_nav(site, path, scope_code=aip.scope.code) -}} 18 | {% endblock %} 19 | 20 | {% block content %} 21 | 53 |
    54 | {% with title = aip.title -%} 55 | {% include 'includes/breadcrumb.html.j2' %} 56 | {% endwith -%} 57 | {% include 'includes/state_banners/' + aip.state + '.html.j2' ignore missing %} 58 |

    AIP-{{ aip.id }}

    59 | {{ aip.content.html }} 60 | {% include 'includes/footer.html.j2' %} 61 |
    62 | {% endblock %} 63 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/breadcrumb.html.j2: -------------------------------------------------------------------------------- 1 |
    2 | 31 |
    32 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/footer.html.j2: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/header.html.j2: -------------------------------------------------------------------------------- 1 | 2 |
    3 |
    4 |
    5 | 6 | 23 | 24 |
    25 | 45 |
    46 | 47 |
    48 |
    49 |
    50 |
    51 | 52 | 54 | 59 |
    60 |
    61 | 62 | 63 | View on GitHub 64 | 65 |
    66 |
    67 |
    68 |
    69 | 70 |
    71 |
    72 | 73 |
    74 |
    75 |
    76 |
    77 | 81 |
    82 |
    83 | 93 | 94 | 95 |

    Jump to Content

    96 |
    97 |
    98 |
    99 | 100 |
    101 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/nav.html.j2: -------------------------------------------------------------------------------- 1 | {% macro render_nav(site, path='/', scope_code='general') -%} 2 | 26 | {% endmacro -%} 27 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/state_banners/draft.html.j2: -------------------------------------------------------------------------------- 1 |

    2 | This AIP is currently a draft. This means that it is being actively debated 3 | and discussed, and it may change in non-trivial ways. 4 |

    5 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/state_banners/rejected.html.j2: -------------------------------------------------------------------------------- 1 |

    2 | This AIP has been rejected and is not considered a best practice. 3 |

    4 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/state_banners/replaced.html.j2: -------------------------------------------------------------------------------- 1 |

    2 | This AIP is no longer the best current practice.
    3 | It has been replaced by 4 | AIP-{{ aip.replacement_id }}. 5 |

    6 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/state_banners/reviewing.html.j2: -------------------------------------------------------------------------------- 1 |

    2 | This AIP is currently under review. This means that the editors have read it 3 | and are in high-level concurrence, and while it is not yet fully approved, we 4 | have a good faith expectation that the AIP will be approved in something 5 | close to its current state. 6 |

    7 | -------------------------------------------------------------------------------- /aip_site/support/templates/includes/state_banners/withdrawn.html.j2: -------------------------------------------------------------------------------- 1 |

    2 | This AIP has been withdrawn and is not considered a best practice. 3 |

    4 | -------------------------------------------------------------------------------- /aip_site/support/templates/index.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html.j2' %} 2 | 3 | {% block title %}API Improvement Proposals{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block main %} 10 |
    11 |
    12 |
    13 | API Improvement Proposals 14 |
    15 |
    16 | Focused design documents for flexible API development. 17 |
    18 |
    19 | {% for button in site.config.hero.buttons -%} 20 | 21 | {{ button.text }} 22 | 23 | {% endfor %} 24 |
    25 |
    26 | 27 |
    28 |

    Welcome

    29 |

    30 | AIPs are design documents that summarize Google's API design 31 | decisions. They also provide a framework and system for others to 32 | document their own API design rules and practices. 33 |

    34 |
    35 | 36 |
    37 | {% for shortcut in site.config.hero.shortcuts %} 38 |
    39 |

    {{ shortcut.title }}

    40 |
    {{ shortcut.description }}
    41 | 42 | {{ shortcut.button.text }} » 43 | 44 |
    45 | {% endfor %} 46 |
    47 | {% include 'includes/footer.html.j2' %} 48 |
    49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /aip_site/support/templates/layouts/base.html.j2: -------------------------------------------------------------------------------- 1 | {% import 'includes/nav.html.j2' as nav -%} 2 | 3 | 4 | 5 | {%- block title -%}{{- title -}}{%- endblock -%} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% block css -%} 14 | {% endblock -%} 15 | 16 | 17 | 18 | 24 | 25 | {% block js -%} 26 | {% endblock -%} 27 | {# {% seo %} -- port this #} 28 | {# Global site tag (gtag.js) - Google Analytics -#} 29 | 30 | 37 | 38 | 39 | {% include 'includes/header.html.j2' %} 40 |
    41 | {% block main %} 42 |
    43 | {% block nav -%} 44 | {{ nav.render_nav(site, path) -}} 45 | {% endblock -%} 46 |
    47 | {% block content %} 48 | {% endblock %} 49 |
    50 |
    51 | {% endblock %} 52 |
    53 | 54 | 55 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /aip_site/support/templates/page.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html.j2' %} 2 | 3 | {% block title -%} 4 | {{ page.title }} 5 | {%- endblock %} 6 | 7 | {% block content %} 8 | 23 |
    24 | {% with title = page.title -%} 25 | {% include 'includes/breadcrumb.html.j2' %} 26 | {% endwith -%} 27 | {{ page.content.html }} 28 | 29 | {% if 'footer' in page.config -%} 30 |
    {{ page.config.footer | markdown }}
    31 | {% else -%} 32 | {% include 'includes/footer.html.j2' %} 33 | {% endif -%} 34 |
    35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /aip_site/support/templates/redirect.html.j2: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redirecting… 5 | 6 | 7 | 8 | 9 | 10 |

    Redirecting…

    11 | Click here if you are not redirected. 12 | 13 | 14 | -------------------------------------------------------------------------------- /aip_site/support/templates/search.html.j2: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/base.html.j2' %} 2 | 3 | {% block title -%} 4 | Search 5 | {%- endblock %} 6 | 7 | {% block css %} 8 | 9 | 14 | {% endblock %} 15 | 16 | {% block js %} 17 | 18 | 19 | 20 | 21 | {% endblock %} 22 | 23 | {% block content %} 24 |
    25 |
    26 | 39 |
    40 | 41 |

    Search results

    42 |
    43 |
    44 | 45 | 52 | {% endblock %} 53 | -------------------------------------------------------------------------------- /aip_site/support/templates/search.js.j2: -------------------------------------------------------------------------------- 1 | {# This file generates the search content used by Tipur Search. 2 | The actual search implementation is in site/assets/js/search/ 3 | See https://tipue.com/search -#} 4 | var tipuesearch = {'pages': [ 5 | {% for aip in site.aips.values() -%} 6 | { 7 | 'title': {{ aip.title | striptags | tojson }}, 8 | 'text': {{ aip.content.html | striptags | tojson }}, 9 | 'tags': '', 10 | 'url': '{{ site.relative_uri }}{{ aip.relative_uri }}', 11 | }, 12 | {% endfor -%} 13 | ]}; 14 | -------------------------------------------------------------------------------- /aip_site/utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import functools 16 | 17 | 18 | def cached_property(fx): 19 | """Mimic the @property decorator but cache the result.""" 20 | @functools.wraps(fx) 21 | def inner(self): 22 | # Sanity check: If there is no cache at all, create an empty cache. 23 | if not hasattr(self, '_cached_values'): 24 | object.__setattr__(self, '_cached_values', {}) 25 | 26 | # If and only if the function's result is not in the cache, 27 | # run the function. 28 | if fx.__name__ not in self._cached_values: 29 | self._cached_values[fx.__name__] = fx(self) 30 | 31 | # Return the value from cache. 32 | return self._cached_values[fx.__name__] 33 | return property(inner) 34 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import io 16 | import os 17 | 18 | from setuptools import find_packages, setup # type: ignore 19 | 20 | 21 | PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__)) 22 | 23 | with io.open(os.path.join(PACKAGE_ROOT, 'VERSION'), 'r') as version_file: 24 | VERSION = version_file.read().strip() 25 | 26 | with io.open(os.path.join(PACKAGE_ROOT, 'README.md'), 'r') as readme_file: 27 | long_description = readme_file.read().strip() 28 | 29 | setup( 30 | name='aip-site-generator', 31 | version=VERSION, 32 | license='Apache 2.0', 33 | author='Luke Sneeringer', 34 | author_email='lukesneeringer@google.com', 35 | url='https://github.com/aip-dev/site-generator.git', 36 | packages=find_packages(exclude=['tests']), 37 | description='Static site generator for aip.dev and forks.', 38 | long_description=long_description, 39 | long_description_content_type='text/markdown', 40 | entry_points="""[console_scripts] 41 | aip-site-gen=aip_site.cli:publish 42 | aip-site-serve=aip_site.cli:serve 43 | """, 44 | platforms='Posix; MacOS X', 45 | include_package_data=True, 46 | install_requires=( 47 | 'click==8.1.3', 48 | 'flask==2.2.2', 49 | 'itsdangerous==2.1.2', 50 | 'jinja2==3.1.2', 51 | 'markdown==3.4.1', 52 | 'markupsafe==2.1.1', 53 | 'pygments==2.13.0', 54 | 'pymdown-extensions==9.7', 55 | 'pyscss==1.4.0', 56 | 'pyyaml==6.0.1', 57 | 'six==1.16.0', 58 | 'types-Markdown==3.4.2.1', 59 | 'types-PyYAML==6.0.12', 60 | 'werkzeug==2.2.2', 61 | ), 62 | python_requires='>=3.8', 63 | classifiers=[ 64 | 'Development Status :: 4 - Beta', 65 | 'Environment :: Console', 66 | 'Intended Audience :: Developers', 67 | 'License :: OSI Approved :: Apache Software License', 68 | 'Operating System :: POSIX', 69 | 'Programming Language :: Python :: 3.8', 70 | 'Topic :: Software Development :: Code Generators', 71 | ], 72 | zip_safe=False, 73 | ) 74 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import os 16 | 17 | import pytest 18 | 19 | from aip_site.models.site import Site 20 | 21 | 22 | @pytest.fixture 23 | def site(): 24 | root = os.path.realpath(f'{os.path.dirname(__file__)}/test_data') 25 | return Site.load(root) 26 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from unittest import mock 16 | 17 | from click.testing import CliRunner 18 | import flask 19 | 20 | from aip_site import cli 21 | from aip_site.publisher import Publisher 22 | 23 | 24 | def test_publish(): 25 | runner = CliRunner() 26 | with mock.patch.object(Publisher, 'publish_site') as ps: 27 | result = runner.invoke( 28 | cli.publish, 29 | ['tests/test_data/', '/path/to/dest'], 30 | ) 31 | ps.assert_called_once_with() 32 | assert result.exit_code == 0 33 | 34 | 35 | def test_serve(): 36 | runner = CliRunner() 37 | with mock.patch.object(flask.Flask, 'run') as run: 38 | result = runner.invoke(cli.serve, ['tests/test_data/']) 39 | run.assert_called_once_with(host='0.0.0.0', port=4000, debug=True) 40 | assert result.exit_code == 0 41 | -------------------------------------------------------------------------------- /tests/test_data/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This page is special. It is loaded as a page by the static site generator, but 4 | it **must** live at the root of the site's repository because that is where 5 | GitHub expects it. 6 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0031.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: 31 3 | state: approved 4 | created: 1831-03-16 5 | placement: 6 | category: hugo 7 | order: 10 8 | --- 9 | 10 | # The Hunchback of Notre Dame 11 | 12 | A few years ago, while visiting or, rather, rummaging about Notre-Dame, the 13 | author of this book found, in an obscure nook of one of the towers, the 14 | following word, engraved by hand upon the wall: 15 | 16 | ## Guidance 17 | 18 | ``` 19 | ἈΝÁΓΚΗ. 20 | ``` 21 | 22 | These Greek capitals, black with age, and quite deeply graven in the stone, 23 | with I know not what signs peculiar to Gothic caligraphy imprinted upon their 24 | forms and upon their attitudes, as though with the purpose of revealing that it 25 | had been a hand of the Middle Ages which had inscribed them there, and 26 | especially the fatal and melancholy meaning contained in them, struck the 27 | author deeply. 28 | 29 | ## Further reading 30 | 31 | - For more from Victor Hugo, read AIP-62. 32 | - For Dickens instead, consult [AIP-43](./0043.md). 33 | - More Dickens is available in [A Tale of Two Cities][aip-59]. 34 | - If you like poetry, AIP-1609 or [Orpheus][aip-1622] might be for you. 35 | 36 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0038/aip.md: -------------------------------------------------------------------------------- 1 | # Oliver Twist 2 | 3 | Oliver Twist is a 1838 novel by the British novelist Charles Dickens. 4 | 5 | ## Guidance 6 | 7 | Among other public buildings in a certain town, which for many reasons it will 8 | be prudent to refrain from mentioning, and to which I will assign no fictitious 9 | name, there is one anciently common to most towns, great or small: to wit, a 10 | workhouse; and in this workhouse was born; on a day and date which I need not 11 | trouble myself to repeat, inasmuch as it can be of no possible consequence to 12 | the reader, in this stage of the business at all events; the item of mortality 13 | whose name is prefixed to the head of this chapter. 14 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0038/aip.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: 38 3 | state: approved 4 | created: 1838-04-21 5 | placement: 6 | category: dickens 7 | order: 5 8 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0043.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: 43 3 | state: reviewing 4 | created: 1843-12-25 5 | placement: 6 | category: dickens 7 | order: 10 8 | changelog: 9 | - date: 1844-12-25 10 | message: Local reading in London 11 | --- 12 | 13 | # A Christmas Carol 14 | 15 | Marley was dead: to begin with. There is no doubt whatever about that. The 16 | register of his burial was signed by the clergyman, the clerk, the undertaker, 17 | and the chief mourner. Scrooge signed it: and Scrooge’s name was good upon 18 | ’Change, for anything he chose to put his hand to. Old Marley was as dead as a 19 | door-nail. 20 | 21 | ## Guidance 22 | 23 | Mind! I don’t mean to say that I know, of my own knowledge, what there is 24 | particularly dead about a door-nail. I might have been inclined, myself, to 25 | regard a coffin-nail as the deadest piece of ironmongery in the trade. But the 26 | wisdom of our ancestors is in the simile; and my unhallowed hands shall not 27 | disturb it, or the Country’s done for. You will therefore permit me to repeat, 28 | emphatically, that Marley was as dead as a door-nail. 29 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0059.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: 59 3 | state: approved 4 | created: 1859-04-21 5 | placement: 6 | category: dickens 7 | order: 20 8 | redirect_from: /two-cities 9 | --- 10 | 11 | # A Tale of Two Cities 12 | 13 | It was the best of times, it was the worst of times, it was the age of wisdom, 14 | it was the age of foolishness, it was the epoch of belief, it was the epoch of 15 | incredulity, it was the season of Light, it was the season of Darkness, it was 16 | the spring of hope, it was the winter of despair, we had everything before us, 17 | we had nothing before us, we were all going direct to Heaven, we were all going 18 | direct the other way—in short, the period was so far like the present period, 19 | that some of its noisiest authorities insisted on its being received, for good 20 | or for evil, in the superlative degree of comparison only. 21 | 22 | ## Guidance 23 | 24 | There were a king with a large jaw and a queen with a plain face, on the throne 25 | of England; there were a king with a large jaw and a queen with a fair face, on 26 | the throne of France. In both countries it was clearer than crystal to the 27 | lords of the State preserves of loaves and fishes, that things in general were 28 | settled for ever. 29 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0062/aip.en.md.j2: -------------------------------------------------------------------------------- 1 | {% extends aip.templates.generic %} 2 | 3 | {% block bp_myriel %} 4 | ## Bishop Myriel 5 | 6 | Although this detail has no connection whatever with the real substance of what 7 | we are about to relate, it will not be superfluous, if merely for the sake of 8 | exactness in all points, to mention here the various rumors and remarks which 9 | had been in circulation about him from the very moment when he arrived in the 10 | diocese. True or false, that which is said of men often occupies as important a 11 | place in their lives, and above all in their destinies, as that which they do. 12 | M. Myriel was the son of a councillor of the Parliament of Aix; hence he 13 | belonged to the nobility of the bar. It was said that his father, destining him 14 | to be the heir of his own post, had married him at a very early age, eighteen 15 | or twenty, in accordance with a custom which is rather widely prevalent in 16 | parliamentary families. In spite of this marriage, however, it was said that 17 | Charles Myriel created a great deal of talk. He was well formed, though rather 18 | short in stature, elegant, graceful, intelligent; the whole of the first 19 | portion of his life had been devoted to the world and to gallantry. 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0062/aip.md.j2: -------------------------------------------------------------------------------- 1 | # Les Misérables 2 | 3 | Les Misérables is a 1862 novel by the French novelist Victor Hugo. 4 | 5 | ## Guidance 6 | 7 | In 1815, M. Charles-François-Bienvenu Myriel was Bishop of Digne. He was an old 8 | man of about seventy-five years of age; he had occupied the see of Digne 9 | since 1806. 10 | 11 | ### Bp. Myriel 12 | 13 | Quoique ce détail ne touche en aucune manière au fond même de ce que nous avons 14 | à raconter, il n'est peut-être pas inutile, ne fût-ce que pour être exact en 15 | tout, d'indiquer ici les bruits et les propos qui avaient couru sur son compte 16 | au moment où il était arrivé dans le diocèse. Vrai ou faux, ce qu'on dit des 17 | hommes tient souvent autant de place dans leur vie et surtout dans leur 18 | destinée que ce qu'ils font. M. Myriel était fils d'un conseiller au parlement 19 | d'Aix; noblesse de robe. On contait de lui que son père, le réservant pour 20 | hériter de sa charge, l'avait marié de fort bonne heure, à dix-huit ou vingt 21 | ans, suivant un usage assez répandu dans les familles parlementaires. Charles 22 | Myriel, nonobstant ce mariage, avait, disait-on, beaucoup fait parler de lui. 23 | Il était bien fait de sa personne, quoique d'assez petite taille, élégant, 24 | gracieux, spirituel; toute la première partie de sa vie avait été donnée au 25 | monde et aux galanteries. La révolution survint, les événements se 26 | précipitèrent, les familles parlementaires décimées, chassées, traquées, se 27 | dispersèrent. M. Charles Myriel, dès les premiers jours de la révolution, 28 | émigra en Italie. Sa femme y mourut d'une maladie de poitrine dont elle était 29 | atteinte depuis longtemps. Ils n'avaient point d'enfants. Que se passa-t-il 30 | ensuite dans la destinée de M. Myriel? L'écroulement de l'ancienne société 31 | française, la chute de sa propre famille, les tragiques spectacles de 93, plus 32 | effrayants encore peut-être pour les émigrés qui les voyaient de loin avec le 33 | grossissement de l'épouvante, firent-ils germer en lui des idées de renoncement 34 | et de solitude? Fut-il, au milieu d'une de ces distractions et de ces 35 | affections qui occupaient sa vie, subitement atteint d'un de ces coups 36 | mystérieux et terribles qui viennent quelquefois renverser, en le frappant au 37 | cœur, l'homme que les catastrophes publiques n'ébranleraient pas en le frappant 38 | dans son existence et dans sa fortune? Nul n'aurait pu le dire; tout ce qu'on 39 | savait, c'est que, lorsqu'il revint d'Italie, il était prêtre. 40 | 41 | ## Interface definitions 42 | 43 | {% tab proto %} 44 | 45 | {% sample 'les_mis.proto', 'message Book' %} 46 | 47 | {% tab oas %} 48 | 49 | {% sample 'les_mis.oas.yaml', 'components' %} 50 | 51 | {% endtabs %} 52 | 53 | ## Reading a book 54 | 55 | {% tab proto %} 56 | 57 | {% sample 'les_mis.proto', 'rpc GetBook', 'message GetBookRequest' %} 58 | 59 | {% tab oas %} 60 | 61 | {% sample 'les_mis.oas.yaml', '/publishers/{publisherId}/books/{bookId}' %} 62 | 63 | {% endtabs %} 64 | 65 | ## Further reading 66 | 67 | - For more from Victor Hugo, read [AIP-31](/31). 68 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0062/aip.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | id: 62 3 | state: approved 4 | created: 1862-04-21 5 | placement: 6 | category: hugo 7 | order: 20 8 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0062/les_mis.oas.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Library 4 | version: 1.0.0 5 | paths: 6 | /publishers/{publisherId}/books/{bookId}: 7 | get: 8 | operationId: getBook 9 | description: Retrieve a single book. 10 | responses: 11 | 200: 12 | description: OK 13 | content: 14 | application/json: 15 | schema: 16 | $ref: '#/components/schemas/Book' 17 | components: 18 | schema: 19 | Book: 20 | description: A representation of a single book. 21 | properties: 22 | name: 23 | type: string 24 | description: | 25 | The name of the book. 26 | Format: publishers/{publisher}/books/{book} 27 | isbn: 28 | type: string 29 | description: | 30 | The ISBN (International Standard Book Number) for this book. 31 | title: 32 | type: string 33 | description: The title of the book. 34 | authors: 35 | type: array 36 | items: 37 | type: string 38 | description: The author or authors of the book. 39 | rating: 40 | type: float 41 | description: The rating assigned to the book. 42 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/0062/les_mis.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // https://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | import "google/api/annotations.proto"; 18 | 19 | // A trivial library service. 20 | service Library { 21 | // Get a single book. 22 | rpc GetBook(GetBookRequest) returns (Book) { 23 | option (google.api.http) = { 24 | get: "/v1/{name=publishers/*/books/*}" 25 | }; 26 | } 27 | } 28 | 29 | // A representation of a book. 30 | message Book { 31 | string name = 1; 32 | string title = 2; 33 | repeated string authors = 3; 34 | float rating = 4; 35 | } 36 | 37 | // Request message for GetBook. 38 | message GetBookRequest { 39 | string name = 1; 40 | } 41 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/red-herring: -------------------------------------------------------------------------------- 1 | This is an arbitrary, unrecognized file that the site generator should ignore. 2 | -------------------------------------------------------------------------------- /tests/test_data/aip/general/scope.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | order: 0 3 | categories: 4 | - code: hugo 5 | title: Victor Hugo 6 | - code: dickens 7 | -------------------------------------------------------------------------------- /tests/test_data/aip/poetry/1609.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: 1609 3 | state: draft 4 | created: 1609-05-20 5 | --- 6 | 7 | # Sonnet 1 8 | 9 | Sonnets have 14 lines: 10 | 11 | 1. From fairest creatures we desire increase, 12 | 2. That thereby beauty’s rose might never die, 13 | 3. But as the riper should by time decrease, 14 | 4. His tender heir might bear his memory: 15 | 5. But thou, contracted to thine own bright eyes, 16 | 6. Feed’st thy light’s flame with self-substantial fuel. 17 | 7. Making a famine where abundance lies, 18 | 8. Thyself thy foe, to thy sweet self too cruel. 19 | 9. Thou that are now the world’s fresh ornament 20 | 10. And only herald to the gaudy spring, 21 | 11. Within thine own bud buriest thy content 22 | 12. And, tender churl, makest waste in niggarding. 23 | 13. Pity the world, or else this glutton be, 24 | 14. To eat the world’s due, by the grave and thee. 25 | 26 | ## Guidance 27 | 28 | If a sonnet doth not have 14 lines, tis not a sonnet. 29 | -------------------------------------------------------------------------------- /tests/test_data/aip/poetry/1622.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: 1622 3 | state: approved 4 | created: 1622-01-01 5 | --- 6 | 7 | # Orpheus 8 | 9 | William Shakespeare 10 | 11 | Orpheus with his lute made trees 12 | And the mountain tops that freeze 13 | Bow themselves when he did sing: 14 | To his music plants and flowers 15 | Ever sprung; as sun and showers 16 | There had made a lasting spring. 17 | 18 | Every thing that heard him play, 19 | Even the billows of the sea, 20 | Hung their heads and then lay by. 21 | In sweet music is such art, 22 | Killing care and grief of heart 23 | Fall asleep, or hearing, die. 24 | 25 | ## Guidance 26 | 27 | Read more poetry. 28 | -------------------------------------------------------------------------------- /tests/test_data/aip/poetry/scope.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shakespearean Poetry 3 | order: 10 4 | -------------------------------------------------------------------------------- /tests/test_data/aip/red-herring: -------------------------------------------------------------------------------- 1 | This is a random file that the site generator should ignore. 2 | -------------------------------------------------------------------------------- /tests/test_data/config/ext.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - us 3 | - en 4 | - generic 5 | -------------------------------------------------------------------------------- /tests/test_data/config/header.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | links: 3 | - title: Browse AIPs 4 | relative_url: /general 5 | - title: Page 6 | relative_url: /page 7 | - title: FAQ 8 | relative_url: /faq 9 | - title: Contributing 10 | relative_url: /contributing 11 | - title: API Linter 12 | absolute_url: https://linter.aip.dev/ 13 | -------------------------------------------------------------------------------- /tests/test_data/config/hero.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | buttons: 3 | - text: Explore the AIPs 4 | href: /general 5 | - text: Learn how it works 6 | href: /1 7 | shortcuts: 8 | - title: Curious about the basics? 9 | description: | 10 | AIPs are a combination of design guidance and a system we use to manage 11 | and track that guidance. Learn more about how the AIP program works in 12 | the first AIP! 13 | button: 14 | href: /1 15 | text: Read AIP-1 16 | - title: Want to help? 17 | description: | 18 | Interested in helping with AIPs? Contribute by proposing new guidance, 19 | commenting on existing AIPs, or fixing typos. All contributions are 20 | welcome! 21 | button: 22 | href: /contributing 23 | text: Contribute to the project 24 | - title: Want to use AIPs for your organization? 25 | description: | 26 | AIPs are designed to be useful outside of Google. Take a look at how you 27 | might choose which AIPs are best suited to your API design needs. 28 | button: 29 | href: /adopting 30 | text: Learn more 31 | - title: Still have questions? 32 | description: | 33 | Free free to take a look at the frequently asked 34 | questions. If you don't find an answer there, file an issue on our 35 | GitHub repository. 36 | button: 37 | href: https://github.com/googleapis/aip/issues 38 | text: Ask us on GitHub 39 | -------------------------------------------------------------------------------- /tests/test_data/config/urls.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | site: https://aip.dev/ 3 | repo: https://github.com/aip-dev/site-generator 4 | -------------------------------------------------------------------------------- /tests/test_data/pages/authors/dickens.md: -------------------------------------------------------------------------------- 1 | # Charles Dickens 2 | 3 | Charles John Huffam Dickens (7 February 1812 – 9 June 1870) was an English 4 | writer and social critic. He created some of the world's best-known fictional 5 | characters and is regarded by many as the greatest novelist of the Victorian 6 | era. His works enjoyed unprecedented popularity during his lifetime, and by the 7 | 20th century, critics and scholars had recognised him as a literary genius. His 8 | novels and short stories are still widely read today. 9 | -------------------------------------------------------------------------------- /tests/test_data/pages/authors/hugo.md: -------------------------------------------------------------------------------- 1 | # Victor Hugo 2 | 3 | Victor Marie Hugo (26 February 1802) – 22 May 1885) was a French poet, 4 | novelist, and dramatist of the Romantic movement. During a literary career that 5 | spanned more than sixty years, he wrote abundantly in an exceptional variety of 6 | genres: lyrics, satires, epics, philosophical poems, epigrams, novels, history, 7 | critical essays, political speeches, funeral orations, diaries, letters public 8 | and private, and dramas in verse and prose. 9 | -------------------------------------------------------------------------------- /tests/test_data/pages/general/faq.md.j2: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ## What is your name? 4 | 5 | Arthur, King of the Britons 6 | 7 | ## What is your quest? 8 | 9 | To seek the Holy Grail. 10 | 11 | ## What is the capital of Assyria? 12 | 13 | Ninevah 14 | 15 | ## What is your favorite color? 16 | 17 | {% for color in ['blue', 'yellow'] -%} 18 | - {{ color.capitalize() }} 19 | {% endfor -%} 20 | -------------------------------------------------------------------------------- /tests/test_data/pages/general/licensing.md: -------------------------------------------------------------------------------- 1 | # Content licensing 2 | 3 | We are pleased to license much of the AIP content under terms that explicitly 4 | encourage people to take, modify, reuse, re-purpose, and remix our work as they 5 | see fit. 6 | 7 | You will find the following notice at the bottom of many pages: 8 | 9 | > Except as otherwise noted, the content of this page is licensed under the 10 | > [Creative Commons Attribution 4.0 License][ccal-4.0], and code samples are 11 | > licensed under the [Apache 2.0 License][apache-2.0]. For details, see 12 | > [content licensing][licensing]. 13 | 14 | When you see a page with this notice you are free to use 15 | [nearly everything](#restrictions) on the page in your own creations. For 16 | example, you could quote the text in a book, cut-and-paste sections to your 17 | blog, record it as an audiobook for the visually impaired, or even translate it 18 | into Swahili. Really. That's what open content licenses are all about. We just 19 | ask that you give us [attribution](#attribution) when you reuse our work. 20 | 21 | In addition, you are free to use the computer source code that appears in the 22 | content (such as in examples) in the code of your own projects. 23 | 24 | ## Restrictions 25 | 26 | We say "nearly everything" as there are a few simple conditions that apply. 27 | 28 | Google's trademarks and other brand features are not included in this license. 29 | Please see our [standard guidelines for third party use of Google brand 30 | features][branding] for information about this usage. 31 | 32 | In some cases, a page may include content consisting of images, audio or video 33 | material, or a link to content on a different webpage (such as videos or slide 34 | decks). This content is not covered by the license, unless specifically noted. 35 | 36 | ## Attribution 37 | 38 | Proper attribution is required when you reuse or create modified versions of 39 | content that appears on a page made available under the terms of the Creative 40 | Commons Attribution license. The complete requirements for attribution can be 41 | found in section 3 of the [Creative Commons legal code][legal-code]. 42 | 43 | In practice we ask that you provide attribution to Google to the best of the 44 | ability of the medium in which you are producing the work. 45 | 46 | There are several typical ways in which this might apply: 47 | 48 | ### Exact reproductions 49 | 50 | If your online work _exactly reproduces_ text or images from this site, in 51 | whole or in part, please include a paragraph at the bottom of your page that 52 | reads: 53 | 54 | > Portions of this page are reproduced from work created and [shared by 55 | > Google][licensing] and used according to terms described in the [Creative 56 | > Commons 4.0 Attribution License][ccal-4.0]. 57 | 58 | Also, please link back to the original source page so that readers can refer to 59 | it for more information. 60 | 61 | ### Modified versions 62 | 63 | If your online work shows modified text or images based on the content from 64 | this site, please include a paragraph at the bottom of your page that reads: 65 | 66 | > Portions of this page are modifications based on work created and [shared by 67 | > Google][licensing] and used according to terms described in the [Creative 68 | > Commons 4.0 Attribution License][ccal-4.0]. 69 | 70 | Again, please link back to the original source page so that readers can refer 71 | to it for more information. This is even more important when the content has 72 | been modified. 73 | 74 | ### Other media 75 | 76 | If you produce non-hypertext works, such as books, audio, or video, we ask that 77 | you make a best effort to include a spoken or written attribution in the spirit 78 | of the messages above. 79 | 80 | [apache-2.0]: https://www.apache.org/licenses/LICENSE-2.0 81 | [ccal-4.0]: https://creativecommons.org/licenses/by/4.0/ 82 | [branding]: http://www.google.com/permissions/guidelines.html 83 | [legal-code]: https://creativecommons.org/licenses/by/4.0/legalcode 84 | [licensing]: ./licensing.md 85 | -------------------------------------------------------------------------------- /tests/test_data/pages/general/licensing.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | footer: | 3 | This page was adapted from the [site policies][] for developers.google.com. 4 | Last updated: May 13, 2019 5 | 6 | [site policies]: https://developers.google.com/terms/site-policies 7 | -------------------------------------------------------------------------------- /tests/test_data/pages/general/page.md: -------------------------------------------------------------------------------- 1 | # This is a page. 2 | 3 | Is it not amazing? 4 | 5 | ## Information 6 | 7 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 8 | incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis 9 | nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. 10 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu 11 | fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in 12 | culpa qui officia deserunt mollit anim id est laborum. 13 | 14 | ### Real Latin 15 | 16 | Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium 17 | doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore 18 | veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim 19 | ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia 20 | consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque 21 | porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, 22 | adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et 23 | dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis 24 | nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex 25 | ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea 26 | voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem 27 | eum fugiat quo voluptas nulla pariatur? 28 | 29 | ### Fake Latin 30 | 31 | Nam facilisis placerat aliquet. Praesent accumsan lacinia est. Aliquam et 32 | iaculis est. Etiam semper rutrum justo nec pulvinar. Etiam placerat nec nisi a 33 | consequat. Pellentesque dignissim efficitur turpis, at elementum ex pharetra 34 | molestie. Nullam libero lectus, iaculis in tempor ac, fringilla vitae purus. 35 | Integer neque ligula, pulvinar at erat sed, dictum fringilla massa. 36 | 37 | ## Conclusion 38 | 39 | Curabitur egestas, quam vitae scelerisque suscipit, lectus quam posuere diam, 40 | bibendum ultricies libero sem nec tortor. Donec dictum ultricies sapien sed 41 | ultricies. Donec quis nisi eu tellus gravida congue. Nullam vehicula enim nunc, 42 | et facilisis sem maximus sit amet. Sed consectetur placerat metus ac dictum. 43 | Praesent at finibus sem. Praesent vitae tincidunt arcu, vitae facilisis elit. 44 | Mauris non lorem eget mauris ullamcorper tincidunt. 45 | -------------------------------------------------------------------------------- /tests/test_data/pages/red-herring: -------------------------------------------------------------------------------- 1 | This file is not a collection and should not be picked up. 2 | -------------------------------------------------------------------------------- /tests/test_jinja_ext_sample.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from unittest import mock 16 | import io 17 | import textwrap 18 | 19 | import jinja2 20 | import pytest 21 | 22 | from aip_site.jinja import loaders 23 | 24 | 25 | def test_valid_sample(site): 26 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 27 | les_mis = site.aips[62] 28 | les_mis.env.loader.set_template_body(r""" 29 | {% sample 'les_mis.proto', 'message Book' %} 30 | """) 31 | rendered = les_mis.env.get_template('test').render(aip=les_mis) 32 | assert '```proto\n' in rendered 33 | assert 'message Book {' in rendered 34 | assert 'string name = 1;' in rendered 35 | 36 | 37 | def test_valid_sample_nested_braces(site): 38 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 39 | les_mis = site.aips[62] 40 | les_mis.env.loader.set_template_body(r""" 41 | {% sample 'les_mis.proto', 'message Book' %} 42 | """) 43 | content = textwrap.dedent(""" 44 | // A representation for a book. 45 | message Book { 46 | string name = 1; 47 | enum Type { 48 | TYPE_UNSPECIFIED = 0; 49 | HARDCOVER = 1; 50 | } 51 | Type type = 2; 52 | } 53 | 54 | message Other { 55 | string whatever = 1; 56 | } 57 | """.lstrip()) 58 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 59 | rendered = les_mis.env.get_template('test').render(aip=les_mis) 60 | assert '```proto\n' in rendered 61 | assert '// A representation for a book.\n' in rendered 62 | assert 'message Book {\n' in rendered 63 | assert ' string name = 1;\n' in rendered 64 | assert ' enum Type {\n' in rendered 65 | assert ' TYPE_UNSPECIFIED = 0;\n' in rendered 66 | assert ' Type type = 2;\n' in rendered 67 | assert 'message Other' not in rendered 68 | assert 'string whatever' not in rendered 69 | 70 | 71 | def test_valid_sample_semi(site): 72 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 73 | les_mis = site.aips[62] 74 | les_mis.env.loader.set_template_body(r""" 75 | {% sample 'les_mis.proto', 'string name' %} 76 | """) 77 | content = textwrap.dedent(""" 78 | // A representation of a book. 79 | message Book { 80 | // The name of the book. 81 | // Format: publishers/{publisher}/books/{book} 82 | string name = 1; 83 | } 84 | """) 85 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 86 | rendered = les_mis.env.get_template('test').render(aip=les_mis) 87 | assert '```proto\n' in rendered 88 | assert '// The name of the book.' in rendered 89 | assert 'string name = 1;\n' in rendered 90 | assert ' string name = 1;\n' not in rendered 91 | assert '// A representation of a book.\n' not in rendered 92 | assert 'message Book {\n' not in rendered 93 | 94 | 95 | def test_valid_sample_yaml(site): 96 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 97 | les_mis = site.aips[62] 98 | les_mis.env.loader.set_template_body(r""" 99 | {% sample 'les_mis.oas.yaml', 'paths' %} 100 | """) 101 | content = textwrap.dedent(""" 102 | --- 103 | schemas: 104 | meh: meh 105 | # The paths. 106 | paths: 107 | foo: bar 108 | baz: bacon 109 | something_else: false 110 | """) 111 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 112 | rendered = les_mis.env.get_template('test').render(aip=les_mis) 113 | assert '```yaml\n' in rendered 114 | assert '# The paths.\n' in rendered 115 | assert 'paths:\n' in rendered 116 | assert ' foo: bar\n' in rendered 117 | assert ' baz: bacon\n' in rendered 118 | assert 'schemas:' not in rendered 119 | assert 'something_else:' not in rendered 120 | 121 | 122 | def test_valid_sample_yaml_indented(site): 123 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 124 | les_mis = site.aips[62] 125 | les_mis.env.loader.set_template_body(r""" 126 | {% sample 'les_mis.oas.yaml', 'paths' %} 127 | """) 128 | content = textwrap.dedent(""" 129 | --- 130 | schemas: 131 | meh: meh 132 | # The paths. 133 | paths: 134 | foo: bar 135 | baz: bacon 136 | something_else: false 137 | """) 138 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 139 | rendered = les_mis.env.get_template('test').render(aip=les_mis) 140 | assert '```yaml\n' in rendered 141 | assert '# The paths.\n' in rendered 142 | assert 'paths:\n' in rendered 143 | assert ' foo: bar\n' in rendered 144 | assert ' baz: bacon\n' in rendered 145 | assert 'schemas:' not in rendered 146 | assert 'something_else:' not in rendered 147 | 148 | 149 | def test_valid_sample_yaml_with_braces(site): 150 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 151 | les_mis = site.aips[62] 152 | les_mis.env.loader.set_template_body(r""" 153 | {% sample 'les_mis.oas.yaml', '/v1/publishers/{publisherId}' %} 154 | """) 155 | content = textwrap.dedent(""" 156 | --- 157 | paths: 158 | /v1/publishers/{publisherId}: 159 | baz: bacon 160 | something_else: false 161 | """).strip('\n') 162 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 163 | rendered = les_mis.env.get_template('test').render(aip=les_mis) 164 | assert '```yaml\n' in rendered 165 | assert 'paths:\n' not in rendered 166 | assert '/v1/publishers/{publisherId}:\n' in rendered 167 | assert ' baz: bacon\n' in rendered 168 | assert 'something_else:' not in rendered 169 | 170 | 171 | def test_valid_sample_yaml_to_eof(site): 172 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 173 | les_mis = site.aips[62] 174 | les_mis.env.loader.set_template_body(r""" 175 | {% sample 'les_mis.oas.yaml', 'paths' %} 176 | """) 177 | content = textwrap.dedent(""" 178 | --- 179 | paths: 180 | foo: bar 181 | baz: bacon 182 | schemas: 183 | meh: meh 184 | """) 185 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 186 | rendered = les_mis.env.get_template('test').render(aip=les_mis) 187 | assert '```yaml\n' in rendered 188 | assert 'paths:\n' in rendered 189 | assert ' foo: bar\n' in rendered 190 | assert ' baz: bacon\n' in rendered 191 | assert 'schemas:' not in rendered 192 | 193 | 194 | def test_invalid_sample_file_not_found(site): 195 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 196 | les_mis = site.aips[62] 197 | les_mis.env.loader.set_template_body(r""" 198 | {% sample 'bogus.proto', 'message Book' %} 199 | """) 200 | with pytest.raises(jinja2.TemplateSyntaxError) as ex: 201 | les_mis.env.get_template('test').render(aip=les_mis) 202 | assert ex.value.message.startswith('File not found: ') 203 | assert ex.value.message.endswith('bogus.proto') 204 | 205 | 206 | def test_invalid_sample_symbol_not_found(site): 207 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 208 | les_mis = site.aips[62] 209 | les_mis.env.loader.set_template_body(r""" 210 | {% sample 'les_mis.proto', 'message Movie' %} 211 | """) 212 | with pytest.raises(jinja2.TemplateSyntaxError) as ex: 213 | les_mis.env.get_template('test').render(aip=les_mis) 214 | assert ex.value.message.startswith('Symbol not found: ') 215 | assert ex.value.message.endswith('message Movie') 216 | 217 | 218 | def test_invalid_sample_no_open_brace(site): 219 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 220 | les_mis = site.aips[62] 221 | les_mis.env.loader.set_template_body(r""" 222 | {% sample 'les_mis.proto', 'message Book' %} 223 | """) 224 | with pytest.raises(jinja2.TemplateSyntaxError) as ex: 225 | content = 'message Book\n' 226 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 227 | les_mis.env.get_template('test').render(aip=les_mis) 228 | assert ex.value.message.startswith('No block character') 229 | assert ex.value.message.endswith('message Book') 230 | 231 | 232 | def test_invalid_sample_no_close_brace(site): 233 | with mock.patch.object(loaders, 'AIPLoader', TestLoader): 234 | les_mis = site.aips[62] 235 | les_mis.env.loader.set_template_body(r""" 236 | {% sample 'les_mis.proto', 'message Book' %} 237 | """) 238 | with pytest.raises(jinja2.TemplateSyntaxError) as ex: 239 | content = 'message Book {\n' 240 | with mock.patch.object(io, 'open', mock.mock_open(read_data=content)): 241 | les_mis.env.get_template('test').render(aip=les_mis) 242 | assert ex.value.message.startswith('No corresponding }') 243 | assert ex.value.message.endswith('message Book.') 244 | 245 | 246 | class TestLoader(loaders.AIPLoader): 247 | __test__ = False 248 | 249 | def set_template_body(self, text): 250 | self._body = textwrap.dedent(text) 251 | 252 | def list_templates(self): 253 | return super().list_templates() + ['test'] 254 | 255 | def get_source(self, env, template): 256 | if template == 'test': 257 | return self._body, 'test.md.j2', None 258 | return super().get_source(env, template) 259 | -------------------------------------------------------------------------------- /tests/test_jinja_ext_tab.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import textwrap 16 | 17 | import jinja2 18 | 19 | from aip_site.jinja.ext.tab import TabExtension 20 | 21 | 22 | def test_tab(): 23 | t = jinja2.Template(textwrap.dedent(""" 24 | {% tab proto %} 25 | Something something 26 | More more more 27 | {% endtabs %} 28 | """), extensions=[TabExtension]) 29 | rendered = t.render() 30 | assert '=== "Protocol buffers"' in rendered 31 | assert ' Something something\n' in rendered 32 | assert ' More more more\n' in rendered 33 | 34 | 35 | def test_multiple_tabs(): 36 | t = jinja2.Template(textwrap.dedent(""" 37 | {% tab proto %} 38 | Something something 39 | {% tab oas %} 40 | Something else 41 | {% endtabs %} 42 | """), extensions=[TabExtension]) 43 | rendered = t.render() 44 | assert '=== "Protocol buffers"' in rendered 45 | assert '=== "OpenAPI 3.0"' in rendered 46 | -------------------------------------------------------------------------------- /tests/test_jinja_loaders.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import jinja2 16 | import pytest 17 | 18 | 19 | def test_list_templates(site): 20 | assert site.aips[62].env.list_templates() == ['en', 'generic'] 21 | assert site.aips[38].env.list_templates() == ['generic'] 22 | 23 | 24 | def test_get_template(site): 25 | assert isinstance( 26 | site.aips[62].env.get_template('generic'), 27 | jinja2.Template, 28 | ) 29 | with pytest.raises(jinja2.TemplateNotFound): 30 | site.aips[62].env.get_template('bogus') 31 | 32 | 33 | def test_template_auto_blocks(site): 34 | generic = site.aips[62].env.get_template('generic') 35 | assert tuple(generic.blocks.keys()) == ( 36 | 'guidance', 37 | 'bp_myriel', 38 | 'interface_definitions', 39 | 'reading_a_book', 40 | 'further_reading', 41 | ) 42 | -------------------------------------------------------------------------------- /tests/test_md.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import textwrap 16 | 17 | import pytest 18 | 19 | from aip_site import md 20 | 21 | 22 | @pytest.fixture 23 | def markdown_doc(): 24 | return md.MarkdownDocument(textwrap.dedent(""" 25 | # Title 26 | 27 | This is an intro paragraph. 28 | 29 | ## Sub 1 30 | 31 | Stuff and things, things and stuff. 32 | 33 | ### Drilling down 34 | 35 | This had a deeper item. 36 | 37 | ```python 38 | # This is a code example. 39 | ``` 40 | 41 | ### Still drilling 42 | 43 | Drill drill drill 44 | 45 | ## Sub 2 46 | 47 | Weeeee 48 | """).strip()) 49 | 50 | 51 | def test_blocked_content(markdown_doc): 52 | blocked = markdown_doc.blocked_content 53 | blocks = ('sub_1', 'drilling_down', 'still_drilling', 'sub_2') 54 | for ix, block in enumerate(blocks): 55 | assert f'{{% block {block} %}}' in blocked 56 | assert f'{{% endblock %}} {{# {block} #}}' in blocked 57 | 58 | # Also ensure that the blocks both open and close in the correct order. 59 | now_or_later = ( 60 | ('{% block sub_1 %}', '{% block drilling_down %}'), 61 | ('{% block sub_1 %}', '{% block still_drilling %}'), 62 | ('{% block sub_1 %}', '{% block sub_2 %}'), 63 | ('{% endblock %} {# drilling_down #}', '{% block still_drilling %}'), 64 | ('{% endblock %} {# drilling_down #}', '{% endblock %} {# sub_1 #}'), 65 | ('{% endblock %} {# still_drilling #}', '{% endblock %} {# sub_1 #}'), 66 | ('{% endblock %} {# sub_1 #}', '{% endblock %} {# sub_2 #}'), 67 | ) 68 | for now, later in now_or_later: 69 | assert blocked.index(now) < blocked.index(later) 70 | 71 | 72 | def test_blocked_content_error_boundaries(markdown_doc): 73 | # Maliciously screw up the toc since I do not actually know how 74 | # to trigger these error cases otherwise. 75 | markdown_doc.html 76 | markdown_doc._engine.toc_tokens.append({ 77 | 'level': 2, 78 | 'id': 'missing', 79 | 'name': 'Missing', 80 | 'children': [], 81 | }) 82 | 83 | # Some blocks would actually be added in this situation, but this is 84 | # undefined behavior. We just test that we get content back. 85 | assert isinstance(markdown_doc.blocked_content, str) 86 | assert '### Drilling down' in markdown_doc.blocked_content 87 | 88 | 89 | def test_coerce(markdown_doc): 90 | assert '# Title' in markdown_doc 91 | assert '# Title' in str(markdown_doc) 92 | 93 | 94 | def test_html(markdown_doc): 95 | assert '

    Title

    ' in markdown_doc.html 96 | assert '

    Sub 1

    ' in markdown_doc.html 97 | assert '

    Still drilling

    ' in markdown_doc.html 98 | assert '

    Stuff and things, things and stuff.

    ' in markdown_doc.html 99 | assert '
    ' in markdown_doc.html 100 | assert markdown_doc.html is markdown_doc.html 101 | 102 | 103 | def test_title(markdown_doc): 104 | assert markdown_doc.title == 'Title' 105 | 106 | 107 | def test_toc(markdown_doc): 108 | assert 'Sub 1' in markdown_doc.toc 109 | assert 'Drilling down' in markdown_doc.toc 110 | assert 'Still drilling' in markdown_doc.toc 111 | assert 'Sub 2' in markdown_doc.toc 112 | assert 'Title' not in markdown_doc.toc # Note: not in. 113 | assert 'Stuff and things' not in markdown_doc.toc 114 | -------------------------------------------------------------------------------- /tests/test_models_aip.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from datetime import date 16 | 17 | from aip_site.models.aip import Change 18 | 19 | 20 | def test_content(site): 21 | # Test with a changelog. 22 | christmas = site.aips[43] 23 | assert 'Marley was dead' in christmas.content 24 | assert '## Changelog' in christmas.content 25 | 26 | # Test without a changelog. 27 | tale = site.aips[59] 28 | assert 'It was the best of times' in tale.content 29 | assert 'Changelog' not in tale.content 30 | 31 | 32 | def test_content_hotlinking(site): 33 | hunchback = site.aips[31] 34 | les_mis = site.aips[62] 35 | assert '[AIP-62](/62)' in hunchback.content 36 | assert '[Orpheus](/1622)' in hunchback.content 37 | assert '[AIP-1609](/1609)' in hunchback.content 38 | assert '[A Tale of Two Cities](/59)' in hunchback.content 39 | assert '[AIP-31](/31)' in les_mis.content 40 | assert '[[AIP-31](/31)](/31)' not in les_mis.content 41 | 42 | 43 | def test_content_hotlinking_relative_uri(site): 44 | site.config['urls']['site'] = 'https://github.io/aip' 45 | hunchback = site.aips[31] 46 | assert '[AIP-62](/aip/62)' in hunchback.content 47 | 48 | 49 | def test_placement(site): 50 | christmas = site.aips[43] 51 | assert christmas.placement.category == 'dickens' 52 | assert christmas.placement.order == 10 53 | sonnet = site.aips[1609] 54 | assert sonnet.placement.category == 'poetry' 55 | assert sonnet.placement.order == float('inf') 56 | 57 | 58 | def test_redirects(site): 59 | assert site.aips[43].redirects == {'/0043', '/043'} 60 | assert site.aips[59].redirects == {'/0059', '/059', '/two-cities'} 61 | assert site.aips[1622].redirects == {'/1622'} 62 | 63 | 64 | def test_relative_uri(site): 65 | assert site.aips[43].relative_uri == '/43' 66 | assert site.aips[1622].relative_uri == '/poetry/1622' 67 | 68 | 69 | def test_site(site): 70 | assert all([i.site is site for i in site.aips.values()]) 71 | 72 | 73 | def test_title(site): 74 | assert site.aips[43].title == 'A Christmas Carol' 75 | assert site.aips[62].title == 'Les Misérables' 76 | 77 | 78 | def test_updated(site): 79 | assert site.aips[62].updated == date(1862, 4, 21) 80 | assert site.aips[43].updated == date(1844, 12, 25) 81 | 82 | 83 | def test_views(site): 84 | les_mis = site.aips[62] 85 | kw = {'aip': les_mis, 'site': site} 86 | assert 'Quoique ce' in les_mis.templates['generic'].render(**kw) 87 | assert 'Quoique ce' not in les_mis.templates['en'].render(**kw) 88 | assert 'Although' not in les_mis.templates['generic'].render(**kw) 89 | assert 'Although' in les_mis.templates['en'].render(**kw) 90 | assert 'Myriel was Bishop' in les_mis.templates['generic'].render(**kw) 91 | assert 'Myriel was Bishop' in les_mis.templates['en'].render(**kw) 92 | 93 | 94 | def test_render(site): 95 | rendered = site.aips[43].render() 96 | assert '

    A Christmas Carol

    ' in rendered 97 | assert '

    Guidance

    ' in rendered 98 | assert '

    Changelog

    ' in rendered 99 | 100 | 101 | def test_change_ordering(): 102 | a = Change(date=date(2020, 4, 21), message='Eight years') 103 | b = Change(date=date(2012, 4, 21), message='Got married') 104 | assert a < b 105 | -------------------------------------------------------------------------------- /tests/test_models_page.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | def test_collections(site): 17 | assert 'general' in site.collections 18 | assert 'authors' in site.collections 19 | assert 'red-herring' not in site.collections 20 | assert 'dickens' not in site.collections['general'].pages 21 | assert 'faq' not in site.collections['authors'].pages 22 | 23 | 24 | def test_code(site): 25 | assert site.pages['contributing'].code == 'contributing' 26 | assert site.pages['faq'].code == 'faq' 27 | 28 | 29 | def test_content(site): 30 | assert '- Blue\n' in site.pages['faq'].content 31 | assert '- Yellow\n' in site.pages['faq'].content 32 | assert '{% for' not in site.pages['faq'].content 33 | 34 | 35 | def test_relative_uri(site): 36 | assert site.pages['contributing'].relative_uri == '/contributing' 37 | assert site.pages['faq'].relative_uri == '/faq' 38 | assert site.pages['authors/dickens'].relative_uri == '/authors/dickens' 39 | 40 | 41 | def test_repo_path(site): 42 | assert site.pages['contributing'].repo_path == '/CONTRIBUTING.md' 43 | assert site.pages['faq'].repo_path == '/pages/general/faq.md.j2' 44 | assert site.pages['licensing'].repo_path == '/pages/general/licensing.md' 45 | assert site.pages['authors/hugo'].repo_path == '/pages/authors/hugo.md' 46 | 47 | 48 | def test_render(site): 49 | rendered = site.pages['page'].render() 50 | assert '

    This is a page.

    ' in rendered 51 | assert '

    Is it not amazing?

    ' in rendered 52 | assert '

    Information

    ' in rendered 53 | assert '

    Fake Latin

    ' in rendered 54 | assert '

    Real Latin

    ' in rendered 55 | 56 | 57 | def test_render_special_footer(site): 58 | rendered = site.pages['licensing'].render() 59 | assert 'This page was adapted' in rendered 60 | 61 | 62 | def test_title(site): 63 | assert site.pages['contributing'].title == 'Contributing' 64 | assert site.pages['licensing'].title == 'Content licensing' 65 | -------------------------------------------------------------------------------- /tests/test_models_scope.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from aip_site.models.aip import AIP 16 | 17 | 18 | def test_aips(site): 19 | general = site.scopes['general'] 20 | poetry = site.scopes['poetry'] 21 | assert all([i.id < 1000 for i in general.aips.values()]) 22 | assert all([i.id > 1000 for i in poetry.aips.values()]) 23 | assert all([isinstance(i, AIP) for i in general.aips.values()]) 24 | 25 | 26 | def test_categories(site): 27 | general = site.scopes['general'] 28 | poetry = site.scopes['poetry'] 29 | assert len(general.categories) == 2 30 | assert 'hugo' in general.categories 31 | assert 'dickens' in general.categories 32 | assert len(poetry.categories) == 1 33 | assert 'poetry' in poetry.categories 34 | 35 | 36 | def test_relative_uri(site): 37 | for scope in site.scopes.values(): 38 | assert scope.relative_uri == f'/{scope.code}' 39 | 40 | 41 | def test_render(site): 42 | rendered = site.scopes['general'].render() 43 | rendered_poetry = site.scopes['poetry'].render() 44 | assert '

    Victor Hugo

    ' in rendered 45 | assert '

    Dickens

    ' in rendered 46 | assert '

    Shakespearean Poetry

    ' in rendered_poetry 47 | -------------------------------------------------------------------------------- /tests/test_models_site.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from aip_site.models.aip import AIP 16 | from aip_site.models.page import Page 17 | from aip_site.models.scope import Scope 18 | 19 | 20 | def test_aips(site): 21 | assert isinstance(site.aips, dict) 22 | assert all([isinstance(i, AIP) for i in site.aips.values()]) 23 | assert 62 in site.aips 24 | assert 1622 in site.aips 25 | 26 | 27 | def test_base_url(site): 28 | assert site.base_url == 'https://aip.dev' 29 | 30 | 31 | def test_pages(site): 32 | assert isinstance(site.pages, dict) 33 | assert all([isinstance(i, Page) for i in site.pages.values()]) 34 | assert 'licensing' in site.pages 35 | assert 'contributing' in site.pages 36 | 37 | 38 | def test_relative_uri(site): 39 | assert site.relative_uri == '' 40 | site.config['urls']['site'] = 'https://foo.github.io/bar/' 41 | assert site.relative_uri == '/bar' 42 | 43 | 44 | def test_repo_url(site): 45 | assert site.repo_url == 'https://github.com/aip-dev/site-generator' 46 | 47 | 48 | def test_scopes(site): 49 | assert isinstance(site.scopes, dict) 50 | assert all([isinstance(i, Scope) for i in site.scopes.values()]) 51 | assert 'general' in site.scopes 52 | assert 'poetry' in site.scopes 53 | assert 'red-herring' not in site.scopes 54 | -------------------------------------------------------------------------------- /tests/test_publisher.py: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import tempfile 16 | 17 | from aip_site.publisher import Publisher 18 | 19 | 20 | def test_publisher(): 21 | with tempfile.TemporaryDirectory() as tmp: 22 | pub = Publisher('tests/test_data/', tmp) 23 | pub.publish_site() 24 | --------------------------------------------------------------------------------