Home | History | Annotate | Download | only in docs
      1 #
      2 # Copyright (C) 2012 The Android Open Source Project
      3 #
      4 # Licensed under the Apache License, Version 2.0 (the "License");
      5 # you may not use this file except in compliance with the License.
      6 # You may obtain a copy of the License at
      7 #
      8 #      http://www.apache.org/licenses/LICENSE-2.0
      9 #
     10 # Unless required by applicable law or agreed to in writing, software
     11 # distributed under the License is distributed on an "AS IS" BASIS,
     12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13 # See the License for the specific language governing permissions and
     14 # limitations under the License.
     15 #
     16 
     17 """
     18 A set of helpers for rendering Mako templates with a Metadata model.
     19 """
     20 
     21 import metadata_model
     22 import re
     23 import markdown
     24 import textwrap
     25 import sys
     26 import bs4
     27 # Monkey-patch BS4. WBR element must not have an end tag.
     28 bs4.builder.HTMLTreeBuilder.empty_element_tags.add("wbr")
     29 
     30 from collections import OrderedDict
     31 
     32 # Relative path from HTML file to the base directory used by <img> tags
     33 IMAGE_SRC_METADATA="images/camera2/metadata/"
     34 
     35 # Prepend this path to each <img src="foo"> in javadocs
     36 JAVADOC_IMAGE_SRC_METADATA="../../../../" + IMAGE_SRC_METADATA
     37 
     38 _context_buf = None
     39 
     40 def _is_sec_or_ins(x):
     41   return isinstance(x, metadata_model.Section) or    \
     42          isinstance(x, metadata_model.InnerNamespace)
     43 
     44 ##
     45 ## Metadata Helpers
     46 ##
     47 
     48 def find_all_sections(root):
     49   """
     50   Find all descendants that are Section or InnerNamespace instances.
     51 
     52   Args:
     53     root: a Metadata instance
     54 
     55   Returns:
     56     A list of Section/InnerNamespace instances
     57 
     58   Remarks:
     59     These are known as "sections" in the generated C code.
     60   """
     61   return root.find_all(_is_sec_or_ins)
     62 
     63 def find_parent_section(entry):
     64   """
     65   Find the closest ancestor that is either a Section or InnerNamespace.
     66 
     67   Args:
     68     entry: an Entry or Clone node
     69 
     70   Returns:
     71     An instance of Section or InnerNamespace
     72   """
     73   return entry.find_parent_first(_is_sec_or_ins)
     74 
     75 # find uniquely named entries (w/o recursing through inner namespaces)
     76 def find_unique_entries(node):
     77   """
     78   Find all uniquely named entries, without recursing through inner namespaces.
     79 
     80   Args:
     81     node: a Section or InnerNamespace instance
     82 
     83   Yields:
     84     A sequence of MergedEntry nodes representing an entry
     85 
     86   Remarks:
     87     This collapses multiple entries with the same fully qualified name into
     88     one entry (e.g. if there are multiple entries in different kinds).
     89   """
     90   if not isinstance(node, metadata_model.Section) and    \
     91      not isinstance(node, metadata_model.InnerNamespace):
     92       raise TypeError("expected node to be a Section or InnerNamespace")
     93 
     94   d = OrderedDict()
     95   # remove the 'kinds' from the path between sec and the closest entries
     96   # then search the immediate children of the search path
     97   search_path = isinstance(node, metadata_model.Section) and node.kinds \
     98                 or [node]
     99   for i in search_path:
    100       for entry in i.entries:
    101           d[entry.name] = entry
    102 
    103   for k,v in d.iteritems():
    104       yield v.merge()
    105 
    106 def path_name(node):
    107   """
    108   Calculate a period-separated string path from the root to this element,
    109   by joining the names of each node and excluding the Metadata/Kind nodes
    110   from the path.
    111 
    112   Args:
    113     node: a Node instance
    114 
    115   Returns:
    116     A string path
    117   """
    118 
    119   isa = lambda x,y: isinstance(x, y)
    120   fltr = lambda x: not isa(x, metadata_model.Metadata) and \
    121                    not isa(x, metadata_model.Kind)
    122 
    123   path = node.find_parents(fltr)
    124   path = list(path)
    125   path.reverse()
    126   path.append(node)
    127 
    128   return ".".join((i.name for i in path))
    129 
    130 def has_descendants_with_enums(node):
    131   """
    132   Determine whether or not the current node is or has any descendants with an
    133   Enum node.
    134 
    135   Args:
    136     node: a Node instance
    137 
    138   Returns:
    139     True if it finds an Enum node in the subtree, False otherwise
    140   """
    141   return bool(node.find_first(lambda x: isinstance(x, metadata_model.Enum)))
    142 
    143 def get_children_by_throwing_away_kind(node, member='entries'):
    144   """
    145   Get the children of this node by compressing the subtree together by removing
    146   the kind and then combining any children nodes with the same name together.
    147 
    148   Args:
    149     node: An instance of Section, InnerNamespace, or Kind
    150 
    151   Returns:
    152     An iterable over the combined children of the subtree of node,
    153     as if the Kinds never existed.
    154 
    155   Remarks:
    156     Not recursive. Call this function repeatedly on each child.
    157   """
    158 
    159   if isinstance(node, metadata_model.Section):
    160     # Note that this makes jump from Section to Kind,
    161     # skipping the Kind entirely in the tree.
    162     node_to_combine = node.combine_kinds_into_single_node()
    163   else:
    164     node_to_combine = node
    165 
    166   combined_kind = node_to_combine.combine_children_by_name()
    167 
    168   return (i for i in getattr(combined_kind, member))
    169 
    170 def get_children_by_filtering_kind(section, kind_name, member='entries'):
    171   """
    172   Takes a section and yields the children of the merged kind under this section.
    173 
    174   Args:
    175     section: An instance of Section
    176     kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
    177 
    178   Returns:
    179     An iterable over the children of the specified merged kind.
    180   """
    181 
    182   matched_kind = next((i for i in section.merged_kinds if i.name == kind_name), None)
    183 
    184   if matched_kind:
    185     return getattr(matched_kind, member)
    186   else:
    187     return ()
    188 
    189 ##
    190 ## Filters
    191 ##
    192 
    193 # abcDef.xyz -> ABC_DEF_XYZ
    194 def csym(name):
    195   """
    196   Convert an entry name string into an uppercase C symbol.
    197 
    198   Returns:
    199     A string
    200 
    201   Example:
    202     csym('abcDef.xyz') == 'ABC_DEF_XYZ'
    203   """
    204   newstr = name
    205   newstr = "".join([i.isupper() and ("_" + i) or i for i in newstr]).upper()
    206   newstr = newstr.replace(".", "_")
    207   return newstr
    208 
    209 # abcDef.xyz -> abc_def_xyz
    210 def csyml(name):
    211   """
    212   Convert an entry name string into a lowercase C symbol.
    213 
    214   Returns:
    215     A string
    216 
    217   Example:
    218     csyml('abcDef.xyz') == 'abc_def_xyz'
    219   """
    220   return csym(name).lower()
    221 
    222 # pad with spaces to make string len == size. add new line if too big
    223 def ljust(size, indent=4):
    224   """
    225   Creates a function that given a string will pad it with spaces to make
    226   the string length == size. Adds a new line if the string was too big.
    227 
    228   Args:
    229     size: an integer representing how much spacing should be added
    230     indent: an integer representing the initial indendation level
    231 
    232   Returns:
    233     A function that takes a string and returns a string.
    234 
    235   Example:
    236     ljust(8)("hello") == 'hello   '
    237 
    238   Remarks:
    239     Deprecated. Use pad instead since it works for non-first items in a
    240     Mako template.
    241   """
    242   def inner(what):
    243     newstr = what.ljust(size)
    244     if len(newstr) > size:
    245       return what + "\n" + "".ljust(indent + size)
    246     else:
    247       return newstr
    248   return inner
    249 
    250 def _find_new_line():
    251 
    252   if _context_buf is None:
    253     raise ValueError("Context buffer was not set")
    254 
    255   buf = _context_buf
    256   x = -1 # since the first read is always ''
    257   cur_pos = buf.tell()
    258   while buf.tell() > 0 and buf.read(1) != '\n':
    259     buf.seek(cur_pos - x)
    260     x = x + 1
    261 
    262   buf.seek(cur_pos)
    263 
    264   return int(x)
    265 
    266 # Pad the string until the buffer reaches the desired column.
    267 # If string is too long, insert a new line with 'col' spaces instead
    268 def pad(col):
    269   """
    270   Create a function that given a string will pad it to the specified column col.
    271   If the string overflows the column, put the string on a new line and pad it.
    272 
    273   Args:
    274     col: an integer specifying the column number
    275 
    276   Returns:
    277     A function that given a string will produce a padded string.
    278 
    279   Example:
    280     pad(8)("hello") == 'hello   '
    281 
    282   Remarks:
    283     This keeps track of the line written by Mako so far, so it will always
    284     align to the column number correctly.
    285   """
    286   def inner(what):
    287     wut = int(col)
    288     current_col = _find_new_line()
    289 
    290     if len(what) > wut - current_col:
    291       return what + "\n".ljust(col)
    292     else:
    293       return what.ljust(wut - current_col)
    294   return inner
    295 
    296 # int32 -> TYPE_INT32, byte -> TYPE_BYTE, etc. note that enum -> TYPE_INT32
    297 def ctype_enum(what):
    298   """
    299   Generate a camera_metadata_type_t symbol from a type string.
    300 
    301   Args:
    302     what: a type string
    303 
    304   Returns:
    305     A string representing the camera_metadata_type_t
    306 
    307   Example:
    308     ctype_enum('int32') == 'TYPE_INT32'
    309     ctype_enum('int64') == 'TYPE_INT64'
    310     ctype_enum('float') == 'TYPE_FLOAT'
    311 
    312   Remarks:
    313     An enum is coerced to a byte since the rest of the camera_metadata
    314     code doesn't support enums directly yet.
    315   """
    316   return 'TYPE_%s' %(what.upper())
    317 
    318 
    319 # Calculate a java type name from an entry with a Typedef node
    320 def _jtypedef_type(entry):
    321   typedef = entry.typedef
    322   additional = ''
    323 
    324   # Hacky way to deal with arrays. Assume that if we have
    325   # size 'Constant x N' the Constant is part of the Typedef size.
    326   # So something sized just 'Constant', 'Constant1 x Constant2', etc
    327   # is not treated as a real java array.
    328   if entry.container == 'array':
    329     has_variable_size = False
    330     for size in entry.container_sizes:
    331       try:
    332         size_int = int(size)
    333       except ValueError:
    334         has_variable_size = True
    335 
    336     if has_variable_size:
    337       additional = '[]'
    338 
    339   try:
    340     name = typedef.languages['java']
    341 
    342     return "%s%s" %(name, additional)
    343   except KeyError:
    344     return None
    345 
    346 # Box if primitive. Otherwise leave unboxed.
    347 def _jtype_box(type_name):
    348   mapping = {
    349     'boolean': 'Boolean',
    350     'byte': 'Byte',
    351     'int': 'Integer',
    352     'float': 'Float',
    353     'double': 'Double',
    354     'long': 'Long'
    355   }
    356 
    357   return mapping.get(type_name, type_name)
    358 
    359 def jtype_unboxed(entry):
    360   """
    361   Calculate the Java type from an entry type string, to be used whenever we
    362   need the regular type in Java. It's not boxed, so it can't be used as a
    363   generic type argument when the entry type happens to resolve to a primitive.
    364 
    365   Remarks:
    366     Since Java generics cannot be instantiated with primitives, this version
    367     is not applicable in that case. Use jtype_boxed instead for that.
    368 
    369   Returns:
    370     The string representing the Java type.
    371   """
    372   if not isinstance(entry, metadata_model.Entry):
    373     raise ValueError("Expected entry to be an instance of Entry")
    374 
    375   metadata_type = entry.type
    376 
    377   java_type = None
    378 
    379   if entry.typedef:
    380     typedef_name = _jtypedef_type(entry)
    381     if typedef_name:
    382       java_type = typedef_name # already takes into account arrays
    383 
    384   if not java_type:
    385     if not java_type and entry.enum and metadata_type == 'byte':
    386       # Always map byte enums to Java ints, unless there's a typedef override
    387       base_type = 'int'
    388 
    389     else:
    390       mapping = {
    391         'int32': 'int',
    392         'int64': 'long',
    393         'float': 'float',
    394         'double': 'double',
    395         'byte': 'byte',
    396         'rational': 'Rational'
    397       }
    398 
    399       base_type = mapping[metadata_type]
    400 
    401     # Convert to array (enums, basic types)
    402     if entry.container == 'array':
    403       additional = '[]'
    404     else:
    405       additional = ''
    406 
    407     java_type = '%s%s' %(base_type, additional)
    408 
    409   # Now box this sucker.
    410   return java_type
    411 
    412 def jtype_boxed(entry):
    413   """
    414   Calculate the Java type from an entry type string, to be used as a generic
    415   type argument in Java. The type is guaranteed to inherit from Object.
    416 
    417   It will only box when absolutely necessary, i.e. int -> Integer[], but
    418   int[] -> int[].
    419 
    420   Remarks:
    421     Since Java generics cannot be instantiated with primitives, this version
    422     will use boxed types when absolutely required.
    423 
    424   Returns:
    425     The string representing the boxed Java type.
    426   """
    427   unboxed_type = jtype_unboxed(entry)
    428   return _jtype_box(unboxed_type)
    429 
    430 def _is_jtype_generic(entry):
    431   """
    432   Determine whether or not the Java type represented by the entry type
    433   string and/or typedef is a Java generic.
    434 
    435   For example, "Range<Integer>" would be considered a generic, whereas
    436   a "MeteringRectangle" or a plain "Integer" would not be considered a generic.
    437 
    438   Args:
    439     entry: An instance of an Entry node
    440 
    441   Returns:
    442     True if it's a java generic, False otherwise.
    443   """
    444   if entry.typedef:
    445     local_typedef = _jtypedef_type(entry)
    446     if local_typedef:
    447       match = re.search(r'<.*>', local_typedef)
    448       return bool(match)
    449   return False
    450 
    451 def _jtype_primitive(what):
    452   """
    453   Calculate the Java type from an entry type string.
    454 
    455   Remarks:
    456     Makes a special exception for Rational, since it's a primitive in terms of
    457     the C-library camera_metadata type system.
    458 
    459   Returns:
    460     The string representing the primitive type
    461   """
    462   mapping = {
    463     'int32': 'int',
    464     'int64': 'long',
    465     'float': 'float',
    466     'double': 'double',
    467     'byte': 'byte',
    468     'rational': 'Rational'
    469   }
    470 
    471   try:
    472     return mapping[what]
    473   except KeyError as e:
    474     raise ValueError("Can't map '%s' to a primitive, not supported" %what)
    475 
    476 def jclass(entry):
    477   """
    478   Calculate the java Class reference string for an entry.
    479 
    480   Args:
    481     entry: an Entry node
    482 
    483   Example:
    484     <entry name="some_int" type="int32"/>
    485     <entry name="some_int_array" type="int32" container='array'/>
    486 
    487     jclass(some_int) == 'int.class'
    488     jclass(some_int_array) == 'int[].class'
    489 
    490   Returns:
    491     The ClassName.class string
    492   """
    493 
    494   return "%s.class" %jtype_unboxed(entry)
    495 
    496 def jkey_type_token(entry):
    497   """
    498   Calculate the java type token compatible with a Key constructor.
    499   This will be the Java Class<T> for non-generic classes, and a
    500   TypeReference<T> for generic classes.
    501 
    502   Args:
    503     entry: An entry node
    504 
    505   Returns:
    506     The ClassName.class string, or 'new TypeReference<ClassName>() {{ }}' string
    507   """
    508   if _is_jtype_generic(entry):
    509     return "new TypeReference<%s>() {{ }}" %(jtype_boxed(entry))
    510   else:
    511     return jclass(entry)
    512 
    513 def jidentifier(what):
    514   """
    515   Convert the input string into a valid Java identifier.
    516 
    517   Args:
    518     what: any identifier string
    519 
    520   Returns:
    521     String with added underscores if necessary.
    522   """
    523   if re.match("\d", what):
    524     return "_%s" %what
    525   else:
    526     return what
    527 
    528 def enum_calculate_value_string(enum_value):
    529   """
    530   Calculate the value of the enum, even if it does not have one explicitly
    531   defined.
    532 
    533   This looks back for the first enum value that has a predefined value and then
    534   applies addition until we get the right value, using C-enum semantics.
    535 
    536   Args:
    537     enum_value: an EnumValue node with a valid Enum parent
    538 
    539   Example:
    540     <enum>
    541       <value>X</value>
    542       <value id="5">Y</value>
    543       <value>Z</value>
    544     </enum>
    545 
    546     enum_calculate_value_string(X) == '0'
    547     enum_calculate_Value_string(Y) == '5'
    548     enum_calculate_value_string(Z) == '6'
    549 
    550   Returns:
    551     String that represents the enum value as an integer literal.
    552   """
    553 
    554   enum_value_siblings = list(enum_value.parent.values)
    555   this_index = enum_value_siblings.index(enum_value)
    556 
    557   def is_hex_string(instr):
    558     return bool(re.match('0x[a-f0-9]+$', instr, re.IGNORECASE))
    559 
    560   base_value = 0
    561   base_offset = 0
    562   emit_as_hex = False
    563 
    564   this_id = enum_value_siblings[this_index].id
    565   while this_index != 0 and not this_id:
    566     this_index -= 1
    567     base_offset += 1
    568     this_id = enum_value_siblings[this_index].id
    569 
    570   if this_id:
    571     base_value = int(this_id, 0)  # guess base
    572     emit_as_hex = is_hex_string(this_id)
    573 
    574   if emit_as_hex:
    575     return "0x%X" %(base_value + base_offset)
    576   else:
    577     return "%d" %(base_value + base_offset)
    578 
    579 def enumerate_with_last(iterable):
    580   """
    581   Enumerate a sequence of iterable, while knowing if this element is the last in
    582   the sequence or not.
    583 
    584   Args:
    585     iterable: an Iterable of some sequence
    586 
    587   Yields:
    588     (element, bool) where the bool is True iff the element is last in the seq.
    589   """
    590   it = (i for i in iterable)
    591 
    592   first = next(it)  # OK: raises exception if it is empty
    593 
    594   second = first  # for when we have only 1 element in iterable
    595 
    596   try:
    597     while True:
    598       second = next(it)
    599       # more elements remaining.
    600       yield (first, False)
    601       first = second
    602   except StopIteration:
    603     # last element. no more elements left
    604     yield (second, True)
    605 
    606 def pascal_case(what):
    607   """
    608   Convert the first letter of a string to uppercase, to make the identifier
    609   conform to PascalCase.
    610 
    611   If there are dots, remove the dots, and capitalize the letter following
    612   where the dot was. Letters that weren't following dots are left unchanged,
    613   except for the first letter of the string (which is made upper-case).
    614 
    615   Args:
    616     what: a string representing some identifier
    617 
    618   Returns:
    619     String with first letter capitalized
    620 
    621   Example:
    622     pascal_case("helloWorld") == "HelloWorld"
    623     pascal_case("foo") == "Foo"
    624     pascal_case("hello.world") = "HelloWorld"
    625     pascal_case("fooBar.fooBar") = "FooBarFooBar"
    626   """
    627   return "".join([s[0:1].upper() + s[1:] for s in what.split('.')])
    628 
    629 def jkey_identifier(what):
    630   """
    631   Return a Java identifier from a property name.
    632 
    633   Args:
    634     what: a string representing a property name.
    635 
    636   Returns:
    637     Java identifier corresponding to the property name. May need to be
    638     prepended with the appropriate Java class name by the caller of this
    639     function. Note that the outer namespace is stripped from the property
    640     name.
    641 
    642   Example:
    643     jkey_identifier("android.lens.facing") == "LENS_FACING"
    644   """
    645   return csym(what[what.find('.') + 1:])
    646 
    647 def jenum_value(enum_entry, enum_value):
    648   """
    649   Calculate the Java name for an integer enum value
    650 
    651   Args:
    652     enum: An enum-typed Entry node
    653     value: An EnumValue node for the enum
    654 
    655   Returns:
    656     String representing the Java symbol
    657   """
    658 
    659   cname = csym(enum_entry.name)
    660   return cname[cname.find('_') + 1:] + '_' + enum_value.name
    661 
    662 def generate_extra_javadoc_detail(entry):
    663   """
    664   Returns a function to add extra details for an entry into a string for inclusion into
    665   javadoc. Adds information about units, the list of enum values for this key, and the valid
    666   range.
    667   """
    668   def inner(text):
    669     if entry.units:
    670       text += '\n\n<b>Units</b>: %s\n' % (dedent(entry.units))
    671     if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')):
    672       text += '\n\n<b>Possible values:</b>\n<ul>\n'
    673       for value in entry.enum.values:
    674         if not value.hidden:
    675           text += '  <li>{@link #%s %s}</li>\n' % ( jenum_value(entry, value ), value.name )
    676       text += '</ul>\n'
    677     if entry.range:
    678       if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')):
    679         text += '\n\n<b>Available values for this device:</b><br>\n'
    680       else:
    681         text += '\n\n<b>Range of valid values:</b><br>\n'
    682       text += '%s\n' % (dedent(entry.range))
    683     if entry.hwlevel != 'legacy': # covers any of (None, 'limited', 'full')
    684       text += '\n\n<b>Optional</b> - This value may be {@code null} on some devices.\n'
    685     if entry.hwlevel == 'full':
    686       text += \
    687         '\n<b>Full capability</b> - \n' + \
    688         'Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the\n' + \
    689         'android.info.supportedHardwareLevel key\n'
    690     if entry.hwlevel == 'limited':
    691       text += \
    692         '\n<b>Limited capability</b> - \n' + \
    693         'Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the\n' + \
    694         'android.info.supportedHardwareLevel key\n'
    695     if entry.hwlevel == 'legacy':
    696       text += "\nThis key is available on all devices."
    697 
    698     return text
    699   return inner
    700 
    701 
    702 def javadoc(metadata, indent = 4):
    703   """
    704   Returns a function to format a markdown syntax text block as a
    705   javadoc comment section, given a set of metadata
    706 
    707   Args:
    708     metadata: A Metadata instance, representing the the top-level root
    709       of the metadata for cross-referencing
    710     indent: baseline level of indentation for javadoc block
    711   Returns:
    712     A function that transforms a String text block as follows:
    713     - Indent and * for insertion into a Javadoc comment block
    714     - Trailing whitespace removed
    715     - Entire body rendered via markdown to generate HTML
    716     - All tag names converted to appropriate Javadoc {@link} with @see
    717       for each tag
    718 
    719   Example:
    720     "This is a comment for Javadoc\n" +
    721     "     with multiple lines, that should be   \n" +
    722     "     formatted better\n" +
    723     "\n" +
    724     "    That covers multiple lines as well\n"
    725     "    And references android.control.mode\n"
    726 
    727     transforms to
    728     "    * <p>This is a comment for Javadoc\n" +
    729     "    * with multiple lines, that should be\n" +
    730     "    * formatted better</p>\n" +
    731     "    * <p>That covers multiple lines as well</p>\n" +
    732     "    * and references {@link CaptureRequest#CONTROL_MODE android.control.mode}\n" +
    733     "    *\n" +
    734     "    * @see CaptureRequest#CONTROL_MODE\n"
    735   """
    736   def javadoc_formatter(text):
    737     comment_prefix = " " * indent + " * ";
    738 
    739     # render with markdown => HTML
    740     javatext = md(text, JAVADOC_IMAGE_SRC_METADATA)
    741 
    742     # Crossref tag names
    743     kind_mapping = {
    744         'static': 'CameraCharacteristics',
    745         'dynamic': 'CaptureResult',
    746         'controls': 'CaptureRequest' }
    747 
    748     # Convert metadata entry "android.x.y.z" to form
    749     # "{@link CaptureRequest#X_Y_Z android.x.y.z}"
    750     def javadoc_crossref_filter(node):
    751       if node.applied_visibility == 'public':
    752         return '{@link %s#%s %s}' % (kind_mapping[node.kind],
    753                                      jkey_identifier(node.name),
    754                                      node.name)
    755       else:
    756         return node.name
    757 
    758     # For each public tag "android.x.y.z" referenced, add a
    759     # "@see CaptureRequest#X_Y_Z"
    760     def javadoc_see_filter(node_set):
    761       node_set = (x for x in node_set if x.applied_visibility == 'public')
    762 
    763       text = '\n'
    764       for node in node_set:
    765         text = text + '\n@see %s#%s' % (kind_mapping[node.kind],
    766                                       jkey_identifier(node.name))
    767 
    768       return text if text != '\n' else ''
    769 
    770     javatext = filter_tags(javatext, metadata, javadoc_crossref_filter, javadoc_see_filter)
    771 
    772     def line_filter(line):
    773       # Indent each line
    774       # Add ' * ' to it for stylistic reasons
    775       # Strip right side of trailing whitespace
    776       return (comment_prefix + line).rstrip()
    777 
    778     # Process each line with above filter
    779     javatext = "\n".join(line_filter(i) for i in javatext.split("\n")) + "\n"
    780 
    781     return javatext
    782 
    783   return javadoc_formatter
    784 
    785 def dedent(text):
    786   """
    787   Remove all common indentation from every line but the 0th.
    788   This will avoid getting <code> blocks when rendering text via markdown.
    789   Ignoring the 0th line will also allow the 0th line not to be aligned.
    790 
    791   Args:
    792     text: A string of text to dedent.
    793 
    794   Returns:
    795     String dedented by above rules.
    796 
    797   For example:
    798     assertEquals("bar\nline1\nline2",   dedent("bar\n  line1\n  line2"))
    799     assertEquals("bar\nline1\nline2",   dedent(" bar\n  line1\n  line2"))
    800     assertEquals("bar\n  line1\nline2", dedent(" bar\n    line1\n  line2"))
    801   """
    802   text = textwrap.dedent(text)
    803   text_lines = text.split('\n')
    804   text_not_first = "\n".join(text_lines[1:])
    805   text_not_first = textwrap.dedent(text_not_first)
    806   text = text_lines[0] + "\n" + text_not_first
    807 
    808   return text
    809 
    810 def md(text, img_src_prefix=""):
    811     """
    812     Run text through markdown to produce HTML.
    813 
    814     This also removes all common indentation from every line but the 0th.
    815     This will avoid getting <code> blocks in markdown.
    816     Ignoring the 0th line will also allow the 0th line not to be aligned.
    817 
    818     Args:
    819       text: A markdown-syntax using block of text to format.
    820       img_src_prefix: An optional string to prepend to each <img src="target"/>
    821 
    822     Returns:
    823       String rendered by markdown and other rules applied (see above).
    824 
    825     For example, this avoids the following situation:
    826 
    827       <!-- Input -->
    828 
    829       <!--- can't use dedent directly since 'foo' has no indent -->
    830       <notes>foo
    831           bar
    832           bar
    833       </notes>
    834 
    835       <!-- Bad Output -- >
    836       <!-- if no dedent is done generated code looks like -->
    837       <p>foo
    838         <code><pre>
    839           bar
    840           bar</pre></code>
    841       </p>
    842 
    843     Instead we get the more natural expected result:
    844 
    845       <!-- Good Output -->
    846       <p>foo
    847       bar
    848       bar</p>
    849 
    850     """
    851     text = dedent(text)
    852 
    853     # full list of extensions at http://pythonhosted.org/Markdown/extensions/
    854     md_extensions = ['tables'] # make <table> with ASCII |_| tables
    855     # render with markdown
    856     text = markdown.markdown(text, md_extensions)
    857 
    858     # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo">
    859     text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text)
    860     return text
    861 
    862 def filter_tags(text, metadata, filter_function, summary_function = None):
    863     """
    864     Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in
    865     the provided text, and pass them through filter_function and summary_function.
    866 
    867     Used to linkify entry names in HMTL, javadoc output.
    868 
    869     Args:
    870       text: A string representing a block of text destined for output
    871       metadata: A Metadata instance, the root of the metadata properties tree
    872       filter_function: A Node->string function to apply to each node
    873         when found in text; the string returned replaces the tag name in text.
    874       summary_function: A Node list->string function that is provided the list of
    875         unique tag nodes found in text, and which must return a string that is
    876         then appended to the end of the text. The list is sorted alphabetically
    877         by node name.
    878     """
    879 
    880     tag_set = set()
    881     def name_match(name):
    882       return lambda node: node.name == name
    883 
    884     # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure
    885     # to grab .z and not just outer_namespace.x.y.  (sloppy, but since we
    886     # check for validity, a few false positives don't hurt)
    887     for outer_namespace in metadata.outer_namespaces:
    888 
    889       tag_match = outer_namespace.name + \
    890         r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)"
    891 
    892       def filter_sub(match):
    893         whole_match = match.group(0)
    894         section1 = match.group(1)
    895         section2 = match.group(2)
    896         section3 = match.group(3)
    897         end_slash = match.group(4)
    898 
    899         # Don't linkify things ending in slash (urls, for example)
    900         if end_slash:
    901           return whole_match
    902 
    903         candidate = ""
    904 
    905         # First try a two-level match
    906         candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2)
    907         got_two_level = False
    908 
    909         node = metadata.find_first(name_match(candidate2.replace('\n','')))
    910         if not node and '\n' in section2:
    911           # Linefeeds are ambiguous - was the intent to add a space,
    912           # or continue a lengthy name? Try the former now.
    913           candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')])
    914           node = metadata.find_first(name_match(candidate2b))
    915           if node:
    916             candidate2 = candidate2b
    917 
    918         if node:
    919           # Have two-level match
    920           got_two_level = True
    921           candidate = candidate2
    922         elif section3:
    923           # Try three-level match
    924           candidate3 = "%s%s" % (candidate2, section3)
    925           node = metadata.find_first(name_match(candidate3.replace('\n','')))
    926 
    927           if not node and '\n' in section3:
    928             # Linefeeds are ambiguous - was the intent to add a space,
    929             # or continue a lengthy name? Try the former now.
    930             candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')])
    931             node = metadata.find_first(name_match(candidate3b))
    932             if node:
    933               candidate3 = candidate3b
    934 
    935           if node:
    936             # Have 3-level match
    937             candidate = candidate3
    938 
    939         # Replace match with crossref or complain if a likely match couldn't be matched
    940 
    941         if node:
    942           tag_set.add(node)
    943           return whole_match.replace(candidate,filter_function(node))
    944         else:
    945           print >> sys.stderr,\
    946             "  WARNING: Could not crossref likely reference {%s}" % (match.group(0))
    947           return whole_match
    948 
    949       text = re.sub(tag_match, filter_sub, text)
    950 
    951     if summary_function is not None:
    952       return text + summary_function(sorted(tag_set, key=lambda x: x.name))
    953     else:
    954       return text
    955 
    956 def any_visible(section, kind_name, visibilities):
    957   """
    958   Determine if entries in this section have an applied visibility that's in
    959   the list of given visibilities.
    960 
    961   Args:
    962     section: A section of metadata
    963     kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
    964     visibilities: An iterable of visibilities to match against
    965 
    966   Returns:
    967     True if the section has any entries with any of the given visibilities. False otherwise.
    968   """
    969 
    970   for inner_namespace in get_children_by_filtering_kind(section, kind_name,
    971                                                         'namespaces'):
    972     if any(filter_visibility(inner_namespace.merged_entries, visibilities)):
    973       return True
    974 
    975   return any(filter_visibility(get_children_by_filtering_kind(section, kind_name,
    976                                                               'merged_entries'),
    977                                visibilities))
    978 
    979 
    980 def filter_visibility(entries, visibilities):
    981   """
    982   Remove entries whose applied visibility is not in the supplied visibilities.
    983 
    984   Args:
    985     entries: An iterable of Entry nodes
    986     visibilities: An iterable of visibilities to filter against
    987 
    988   Yields:
    989     An iterable of Entry nodes
    990   """
    991   return (e for e in entries if e.applied_visibility in visibilities)
    992 
    993 def remove_synthetic(entries):
    994   """
    995   Filter the given entries by removing those that are synthetic.
    996 
    997   Args:
    998     entries: An iterable of Entry nodes
    999 
   1000   Yields:
   1001     An iterable of Entry nodes
   1002   """
   1003   return (e for e in entries if not e.synthetic)
   1004 
   1005 def wbr(text):
   1006   """
   1007   Insert word break hints for the browser in the form of <wbr> HTML tags.
   1008 
   1009   Word breaks are inserted inside an HTML node only, so the nodes themselves
   1010   will not be changed. Attributes are also left unchanged.
   1011 
   1012   The following rules apply to insert word breaks:
   1013   - For characters in [ '.', '/', '_' ]
   1014   - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
   1015 
   1016   Args:
   1017     text: A string of text containing HTML content.
   1018 
   1019   Returns:
   1020     A string with <wbr> inserted by the above rules.
   1021   """
   1022   SPLIT_CHARS_LIST = ['.', '_', '/']
   1023   SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
   1024   CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
   1025   def wbr_filter(text):
   1026       new_txt = text
   1027 
   1028       # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
   1029       # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
   1030       for words in text.split(" "):
   1031         for char in SPLIT_CHARS_LIST:
   1032           # match at least x.y.z, don't match x or x.y
   1033           if len(words.split(char)) >= CAP_LETTER_MIN:
   1034             new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
   1035             new_txt = new_txt.replace(words, new_word)
   1036 
   1037       # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
   1038       new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
   1039 
   1040       return new_txt
   1041 
   1042   # Do not mangle HTML when doing the replace by using BeatifulSoup
   1043   # - Use the 'html.parser' to avoid inserting <html><body> when decoding
   1044   soup = bs4.BeautifulSoup(text, features='html.parser')
   1045   wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
   1046 
   1047   for navigable_string in soup.findAll(text=True):
   1048       parent = navigable_string.parent
   1049 
   1050       # Insert each '$text<wbr>$foo' before the old '$text$foo'
   1051       split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
   1052       for (split_string, last) in enumerate_with_last(split_by_wbr_list):
   1053           navigable_string.insert_before(split_string)
   1054 
   1055           if not last:
   1056             # Note that 'insert' will move existing tags to this spot
   1057             # so make a new tag instead
   1058             navigable_string.insert_before(wbr_tag())
   1059 
   1060       # Remove the old unmodified text
   1061       navigable_string.extract()
   1062 
   1063   return soup.decode()
   1064