File: //snap/google-cloud-cli/current/lib/googlecloudsdk/calliope/walker_util.py
# -*- coding: utf-8 -*- #
# Copyright 2015 Google LLC. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""A collection of CLI walkers."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import io
import os
from googlecloudsdk.calliope import actions
from googlecloudsdk.calliope import arg_parsers
from googlecloudsdk.calliope import cli_tree
from googlecloudsdk.calliope import markdown
from googlecloudsdk.calliope import walker
from googlecloudsdk.core import properties
from googlecloudsdk.core.document_renderers import render_document
from googlecloudsdk.core.util import files
from googlecloudsdk.core.util import pkg_resources
import six
_HELP_HTML_DATA_FILES = [
    'favicon.ico',
    'index.html',
    '_menu_.css',
    '_menu_.js',
    '_title_.html',
]
class DevSiteGenerator(walker.Walker):
  """Generates DevSite reference HTML in a directory hierarchy.
  This implements gcloud meta generate-help-docs --manpage-dir=DIRECTORY.
  Attributes:
    _directory: The DevSite reference output directory. _need_section_tag[]:
      _need_section_tag[i] is True if there are section subitems at depth i.
      This prevents the creation of empty 'section:' tags in the '_toc' files.
    _toc_root: The root TOC output stream.
    _toc_main: The current main (just under root) TOC output stream.
  """
  _REFERENCE = '/sdk/gcloud/reference'  # TOC reference directory offset.
  _TOC = '_toc.yaml'
  def __init__(
      self, cli, directory, hidden=False, progress_callback=None, restrict=None
  ):
    """Constructor.
    Args:
      cli: The Cloud SDK CLI object.
      directory: The devsite output directory path name.
      hidden: Boolean indicating whether to consider the hidden CLI.
      progress_callback: f(float), The function to call to update the progress
        bar or None for no progress bar.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups.
    """
    super(DevSiteGenerator, self).__init__(cli, restrict=restrict)
    self._directory = directory
    files.MakeDir(self._directory)
    self._need_section_tag = []
    toc_path = os.path.join(self._directory, self._TOC)
    self._toc_root = files.FileWriter(toc_path)
    self._toc_root.write('toc:\n')
    self._toc_root.write('- title: "gcloud Reference"\n')
    self._toc_root.write('  path: %s\n' % self._REFERENCE)
    self._toc_root.write('  section:\n')
    self._toc_main = None
  def Visit(self, node, parent, is_group):
    """Updates the TOC and Renders a DevSite doc for each node in the CLI tree.
    Args:
      node: group/command CommandCommon info.
      parent: The parent Visit() return value, None at the top level.
      is_group: True if node is a group, otherwise its is a command.
    Returns:
      The parent value, ignored here.
    """
    def _UpdateTOC():
      """Updates the DevSIte TOC."""
      depth = len(command) - 1
      if not depth:
        return
      title = ' '.join(command)
      while depth >= len(self._need_section_tag):
        self._need_section_tag.append(False)
      if depth == 1:
        if is_group:
          if self._toc_main:
            # Close the current main group toc if needed.
            self._toc_main.close()
          # Create a new main group toc.
          toc_path = os.path.join(directory, self._TOC)
          toc = files.FileWriter(toc_path)
          self._toc_main = toc
          toc.write('toc:\n')
          toc.write('- title: "%s"\n' % title)
          toc.write('  path: %s\n' % '/'.join([self._REFERENCE] + command[1:]))
          self._need_section_tag[depth] = True
        toc = self._toc_root
        indent = '  '
        if is_group:
          toc.write(
              '%s- include: %s\n'
              % (
                  indent,
                  '/'.join([self._REFERENCE] + command[1:] + [self._TOC]),
              )
          )
          return
      else:
        toc = self._toc_main
        indent = '  ' * (depth - 1)
        if self._need_section_tag[depth - 1]:
          self._need_section_tag[depth - 1] = False
          toc.write('%ssection:\n' % indent)
        title = command[-1]
      toc.write('%s- title: "%s"\n' % (indent, title))
      toc.write(
          '%s  path: %s\n' % (indent, '/'.join([self._REFERENCE] + command[1:]))
      )
      self._need_section_tag[depth] = is_group
    # Set up the destination dir for this level.
    command = node.GetPath()
    if is_group:
      directory = os.path.join(self._directory, *command[1:])
      files.MakeDir(directory, mode=0o755)
    else:
      directory = os.path.join(self._directory, *command[1:-1])
    # Render the DevSite document.
    path = (
        os.path.join(directory, 'index' if is_group else command[-1]) + '.html'
    )
    # Currently, devsite pages from GDU are automatically mirrored to all other
    # universes. To display Universe Disclaimer Information section correctly on
    # all universes after mirroring, temporarily override universe_domain
    # property to force the info section generated in devsite pages.
    universe_domain = None
    if properties.VALUES.core.universe_domain.IsExplicitlySet():
      universe_domain = properties.VALUES.core.universe_domain.Get()
    properties.VALUES.core.universe_domain.Set('universe')
    with files.FileWriter(path) as f:
      md = markdown.Markdown(node)
      render_document.RenderDocument(
          style='devsite',
          title=' '.join(command),
          fin=io.StringIO(md),
          out=f,
          command_node=node,
      )
    # reset universe_domain
    properties.VALUES.core.universe_domain.Set(universe_domain)
    _UpdateTOC()
    return parent
  def Done(self):
    """Closes the TOC files after the CLI tree walk is done."""
    self._toc_root.close()
    if self._toc_main:
      self._toc_main.close()
class HelpTextGenerator(walker.Walker):
  """Generates help text files in a directory hierarchy.
  Attributes:
    _directory: The help text output directory.
  """
  def __init__(
      self, cli, directory, hidden=False, progress_callback=None, restrict=None
  ):
    """Constructor.
    Args:
      cli: The Cloud SDK CLI object.
      directory: The Help Text output directory path name.
      hidden: Boolean indicating whether to consider the hidden CLI.
      progress_callback: f(float), The function to call to update the progress
        bar or None for no progress bar.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups.
    """
    super(HelpTextGenerator, self).__init__(
        cli, progress_callback=progress_callback, restrict=restrict
    )
    self._directory = directory
    files.MakeDir(self._directory)
  def Visit(self, node, parent, is_group):
    """Renders a help text doc for each node in the CLI tree.
    Args:
      node: group/command CommandCommon info.
      parent: The parent Visit() return value, None at the top level.
      is_group: True if node is a group, otherwise its is a command.
    Returns:
      The parent value, ignored here.
    """
    # Set up the destination dir for this level.
    command = node.GetPath()
    if is_group:
      directory = os.path.join(self._directory, *command[1:])
    else:
      directory = os.path.join(self._directory, *command[1:-1])
    files.MakeDir(directory, mode=0o755)
    # Render the help text document.
    path = os.path.join(directory, 'GROUP' if is_group else command[-1])
    with files.FileWriter(path) as f:
      md = markdown.Markdown(node)
      render_document.RenderDocument(style='text', fin=io.StringIO(md), out=f)
    return parent
class DocumentGenerator(walker.Walker):
  """Generates style manpage files with suffix in an output directory.
  All files will be generated in one directory.
  Attributes:
    _directory: The document output directory.
    _style: The document style.
    _suffix: The output file suffix.
  """
  def __init__(self, cli, directory, style, suffix, restrict=None):
    """Constructor.
    Args:
      cli: The Cloud SDK CLI object.
      directory: The manpage output directory path name.
      style: The document style.
      suffix: The generate document file suffix. None for .<SECTION>.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups.
    """
    super(DocumentGenerator, self).__init__(cli, restrict=restrict)
    self._directory = directory
    self._style = style
    self._suffix = suffix
    files.MakeDir(self._directory)
  def Visit(self, node, parent, is_group):
    """Renders document file for each node in the CLI tree.
    Args:
      node: group/command CommandCommon info.
      parent: The parent Visit() return value, None at the top level.
      is_group: True if node is a group, otherwise its is a command.
    Returns:
      The parent value, ignored here.
    """
    if self._style == 'linter':
      meta_data = actions.GetCommandMetaData(node)
    else:
      meta_data = None
    command = node.GetPath()
    path = os.path.join(self._directory, '_'.join(command)) + self._suffix
    with files.FileWriter(path) as f:
      md = markdown.Markdown(node)
      render_document.RenderDocument(
          style=self._style,
          title=' '.join(command),
          fin=io.StringIO(md),
          out=f,
          command_metadata=meta_data,
      )
    return parent
class HtmlGenerator(DocumentGenerator):
  """Generates HTML manpage files with suffix .html in an output directory.
  The output directory will contain a man1 subdirectory containing all of the
  HTML manpage files.
  """
  def WriteHtmlMenu(self, command, out):
    """Writes the command menu tree HTML on out.
    Args:
      command: dict, The tree (nested dict) of command/group names.
      out: stream, The output stream.
    """
    def ConvertPathToIdentifier(path):
      return '_'.join(path)
    def WalkCommandTree(command, prefix):
      """Visit each command and group in the CLI command tree.
      Args:
        command: dict, The tree (nested dict) of command/group names.
        prefix: [str], The subcommand arg prefix.
      """
      level = len(prefix)
      visibility = 'visible' if level <= 1 else 'hidden'
      indent = level * 2 + 2
      name = command.get('_name_')
      args = prefix + [name]
      out.write(
          '{indent}<li class="{visibility}" id="{item}" '
          'onclick="select(event, this.id)">{name}'.format(
              indent=' ' * indent,
              visibility=visibility,
              name=name,
              item=ConvertPathToIdentifier(args),
          )
      )
      commands = command.get('commands', []) + command.get('groups', [])
      if commands:
        out.write('<ul>\n')
        for c in sorted(commands, key=lambda x: x['_name_']):
          WalkCommandTree(c, args)
        out.write('{indent}</ul>\n'.format(indent=' ' * (indent + 1)))
        out.write('{indent}</li>\n'.format(indent=' ' * indent))
      else:
        out.write('</li>\n')
    out.write("""\
<html>
<head>
<meta name="description" content="man page tree navigation">
<meta name="generator" content="gcloud meta generate-help-docs --html-dir=.">
<title> man page tree navigation </title>
<base href="." target="_blank">
<link rel="stylesheet" type="text/css" href="_menu_.css">
<script type="text/javascript" src="_menu_.js"></script>
</head>
<body>
<div class="menu">
 <ul>
""")
    WalkCommandTree(command, [])
    out.write("""\
 </ul>
</div>
</body>
</html>
""")
  def _GenerateHtmlNav(self, directory, cli, hidden, restrict):
    """Generates html nav files in directory."""
    tree = CommandTreeGenerator(cli).Walk(hidden, restrict)
    with files.FileWriter(os.path.join(directory, '_menu_.html')) as out:
      self.WriteHtmlMenu(tree, out)
    for file_name in _HELP_HTML_DATA_FILES:
      file_contents = pkg_resources.GetResource(
          'googlecloudsdk.api_lib.meta.help_html_data.', file_name
      )
      files.WriteBinaryFileContents(
          os.path.join(directory, file_name), file_contents
      )
  def __init__(
      self, cli, directory, hidden=False, progress_callback=None, restrict=None
  ):
    """Constructor.
    Args:
      cli: The Cloud SDK CLI object.
      directory: The HTML output directory path name.
      hidden: Boolean indicating whether to consider the hidden CLI.
      progress_callback: f(float), The function to call to update the progress
        bar or None for no progress bar.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups.
    """
    super(HtmlGenerator, self).__init__(
        cli,
        directory=directory,
        style='html',
        suffix='.html',
        restrict=restrict,
    )
    self._GenerateHtmlNav(directory, cli, hidden, restrict)
class ManPageGenerator(DocumentGenerator):
  """Generates manpage files with suffix .1 in an output directory.
  The output directory will contain a man1 subdirectory containing all of the
  manpage files.
  """
  _SECTION_FORMAT = 'man{section}'
  def __init__(
      self, cli, directory, hidden=False, progress_callback=None, restrict=None
  ):
    """Constructor.
    Args:
      cli: The Cloud SDK CLI object.
      directory: The manpage output directory path name.
      hidden: Boolean indicating whether to consider the hidden CLI.
      progress_callback: f(float), The function to call to update the progress
        bar or None for no progress bar.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups.
    """
    # Currently all gcloud manpages are in section 1.
    section_subdir = self._SECTION_FORMAT.format(section=1)
    section_dir = os.path.join(directory, section_subdir)
    super(ManPageGenerator, self).__init__(
        cli, directory=section_dir, style='man', suffix='.1', restrict=restrict
    )
class LinterGenerator(DocumentGenerator):
  """Generates linter files with suffix .json in an output directory."""
  def __init__(
      self, cli, directory, hidden=False, progress_callback=None, restrict=None
  ):
    """Constructor.
    Args:
      cli: The Cloud SDK CLI object.
      directory: The linter output directory path name.
      hidden: Boolean indicating whether to consider the hidden CLI.
      progress_callback: f(float), The function to call to update the progress
        bar or None for no progress bar.
      restrict: Restricts the walk to the command/group dotted paths in this
        list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
        restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
        commands/groups.
    """
    super(LinterGenerator, self).__init__(
        cli,
        directory=directory,
        style='linter',
        suffix='.json',
        restrict=restrict,
    )
class CommandTreeGenerator(walker.Walker):
  """Constructs a CLI command dict tree.
  This implements the resource generator for gcloud meta list-commands.
  Attributes:
    _with_flags: Include the non-global flags for each command/group if True.
    _with_flag_values: Include flag value choices or :type: if True.
    _global_flags: The set of global flags, only listed for the root command.
  """
  def __init__(self, cli, with_flags=False, with_flag_values=False, **kwargs):
    """Constructor.
    Args:
      cli: The Cloud SDK CLI object.
      with_flags: Include the non-global flags for each command/group if True.
      with_flag_values: Include flags and flag value choices or :type: if True.
      **kwargs: Other keyword arguments to pass to Walker constructor.
    """
    super(CommandTreeGenerator, self).__init__(cli, **kwargs)
    self._with_flags = with_flags or with_flag_values
    self._with_flag_values = with_flag_values
    self._global_flags = set()
  def Visit(self, node, parent, is_group):
    """Visits each node in the CLI command tree to construct the dict tree.
    Args:
      node: group/command CommandCommon info.
      parent: The parent Visit() return value, None at the top level.
      is_group: True if node is a group, otherwise its is a command.
    Returns:
      The subtree parent value, used here to construct a dict tree.
    """
    name = node.name.replace('_', '-')
    info = {'_name_': name}
    if self._with_flags:
      all_flags = []
      for arg in node.GetAllAvailableFlags():
        value = None
        if self._with_flag_values:
          if arg.choices:
            choices = sorted(arg.choices)
            if choices != ['false', 'true']:
              value = ','.join([six.text_type(choice) for choice in choices])
          elif isinstance(arg.type, int):
            value = ':int:'
          elif isinstance(arg.type, float):
            value = ':float:'
          elif isinstance(arg.type, arg_parsers.ArgDict):
            value = ':dict:'
          elif isinstance(arg.type, arg_parsers.ArgList):
            value = ':list:'
          elif arg.nargs != 0:
            metavar = arg.metavar or arg.dest.upper()
            value = ':' + metavar + ':'
        for f in arg.option_strings:
          if value:
            f += '=' + value
          all_flags.append(f)
      no_prefix = '--no-'
      flags = []
      for flag in all_flags:
        if flag in self._global_flags:
          continue
        if flag.startswith(no_prefix):
          positive = '--' + flag[len(no_prefix) :]
          if positive in all_flags:
            continue
        flags.append(flag)
      if flags:
        info['_flags_'] = sorted(flags)
        if not self._global_flags:
          # Most command flags are global (defined by the root command) or
          # command-specific. Group-specific flags are rare. Separating out
          # the global flags streamlines command descriptions and prevents
          # global flag changes (we already have too many!) from making it
          # look like every command has changed.
          self._global_flags.update(flags)
    if is_group:
      if parent:
        if cli_tree.LOOKUP_GROUPS not in parent:
          parent[cli_tree.LOOKUP_GROUPS] = []
        parent[cli_tree.LOOKUP_GROUPS].append(info)
      return info
    if cli_tree.LOOKUP_COMMANDS not in parent:
      parent[cli_tree.LOOKUP_COMMANDS] = []
    parent[cli_tree.LOOKUP_COMMANDS].append(info)
    return None
class GCloudTreeGenerator(walker.Walker):
  """Generates an external representation of the gcloud CLI tree.
  This implements the resource generator for gcloud meta list-gcloud.
  """
  def Visit(self, node, parent, is_group):
    """Visits each node in the CLI command tree to construct the external rep.
    Args:
      node: group/command CommandCommon info.
      parent: The parent Visit() return value, None at the top level.
      is_group: True if node is a group, otherwise its is a command.
    Returns:
      The subtree parent value, used here to construct an external rep node.
    """
    return cli_tree.Command(node, parent)