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     # Identity transform for javadoc links
    743     def javadoc_link_filter(target, shortname):
    744       return '{@link %s %s}' % (target, shortname)
    745 
    746     javatext = filter_links(javatext, javadoc_link_filter)
    747 
    748     # Crossref tag names
    749     kind_mapping = {
    750         'static': 'CameraCharacteristics',
    751         'dynamic': 'CaptureResult',
    752         'controls': 'CaptureRequest' }
    753 
    754     # Convert metadata entry "android.x.y.z" to form
    755     # "{@link CaptureRequest#X_Y_Z android.x.y.z}"
    756     def javadoc_crossref_filter(node):
    757       if node.applied_visibility == 'public':
    758         return '{@link %s#%s %s}' % (kind_mapping[node.kind],
    759                                      jkey_identifier(node.name),
    760                                      node.name)
    761       else:
    762         return node.name
    763 
    764     # For each public tag "android.x.y.z" referenced, add a
    765     # "@see CaptureRequest#X_Y_Z"
    766     def javadoc_crossref_see_filter(node_set):
    767       node_set = (x for x in node_set if x.applied_visibility == 'public')
    768 
    769       text = '\n'
    770       for node in node_set:
    771         text = text + '\n@see %s#%s' % (kind_mapping[node.kind],
    772                                       jkey_identifier(node.name))
    773 
    774       return text if text != '\n' else ''
    775 
    776     javatext = filter_tags(javatext, metadata, javadoc_crossref_filter, javadoc_crossref_see_filter)
    777 
    778     def line_filter(line):
    779       # Indent each line
    780       # Add ' * ' to it for stylistic reasons
    781       # Strip right side of trailing whitespace
    782       return (comment_prefix + line).rstrip()
    783 
    784     # Process each line with above filter
    785     javatext = "\n".join(line_filter(i) for i in javatext.split("\n")) + "\n"
    786 
    787     return javatext
    788 
    789   return javadoc_formatter
    790 
    791 def dedent(text):
    792   """
    793   Remove all common indentation from every line but the 0th.
    794   This will avoid getting <code> blocks when rendering text via markdown.
    795   Ignoring the 0th line will also allow the 0th line not to be aligned.
    796 
    797   Args:
    798     text: A string of text to dedent.
    799 
    800   Returns:
    801     String dedented by above rules.
    802 
    803   For example:
    804     assertEquals("bar\nline1\nline2",   dedent("bar\n  line1\n  line2"))
    805     assertEquals("bar\nline1\nline2",   dedent(" bar\n  line1\n  line2"))
    806     assertEquals("bar\n  line1\nline2", dedent(" bar\n    line1\n  line2"))
    807   """
    808   text = textwrap.dedent(text)
    809   text_lines = text.split('\n')
    810   text_not_first = "\n".join(text_lines[1:])
    811   text_not_first = textwrap.dedent(text_not_first)
    812   text = text_lines[0] + "\n" + text_not_first
    813 
    814   return text
    815 
    816 def md(text, img_src_prefix=""):
    817     """
    818     Run text through markdown to produce HTML.
    819 
    820     This also removes all common indentation from every line but the 0th.
    821     This will avoid getting <code> blocks in markdown.
    822     Ignoring the 0th line will also allow the 0th line not to be aligned.
    823 
    824     Args:
    825       text: A markdown-syntax using block of text to format.
    826       img_src_prefix: An optional string to prepend to each <img src="target"/>
    827 
    828     Returns:
    829       String rendered by markdown and other rules applied (see above).
    830 
    831     For example, this avoids the following situation:
    832 
    833       <!-- Input -->
    834 
    835       <!--- can't use dedent directly since 'foo' has no indent -->
    836       <notes>foo
    837           bar
    838           bar
    839       </notes>
    840 
    841       <!-- Bad Output -- >
    842       <!-- if no dedent is done generated code looks like -->
    843       <p>foo
    844         <code><pre>
    845           bar
    846           bar</pre></code>
    847       </p>
    848 
    849     Instead we get the more natural expected result:
    850 
    851       <!-- Good Output -->
    852       <p>foo
    853       bar
    854       bar</p>
    855 
    856     """
    857     text = dedent(text)
    858 
    859     # full list of extensions at http://pythonhosted.org/Markdown/extensions/
    860     md_extensions = ['tables'] # make <table> with ASCII |_| tables
    861     # render with markdown
    862     text = markdown.markdown(text, md_extensions)
    863 
    864     # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo">
    865     text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text)
    866     return text
    867 
    868 def filter_tags(text, metadata, filter_function, summary_function = None):
    869     """
    870     Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in
    871     the provided text, and pass them through filter_function and summary_function.
    872 
    873     Used to linkify entry names in HMTL, javadoc output.
    874 
    875     Args:
    876       text: A string representing a block of text destined for output
    877       metadata: A Metadata instance, the root of the metadata properties tree
    878       filter_function: A Node->string function to apply to each node
    879         when found in text; the string returned replaces the tag name in text.
    880       summary_function: A Node list->string function that is provided the list of
    881         unique tag nodes found in text, and which must return a string that is
    882         then appended to the end of the text. The list is sorted alphabetically
    883         by node name.
    884     """
    885 
    886     tag_set = set()
    887     def name_match(name):
    888       return lambda node: node.name == name
    889 
    890     # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure
    891     # to grab .z and not just outer_namespace.x.y.  (sloppy, but since we
    892     # check for validity, a few false positives don't hurt).
    893     # Try to ignore items of the form {@link <outer_namespace>...
    894     for outer_namespace in metadata.outer_namespaces:
    895 
    896       tag_match = r"(?<!\{@link\s)" + outer_namespace.name + \
    897         r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)"
    898 
    899       def filter_sub(match):
    900         whole_match = match.group(0)
    901         section1 = match.group(1)
    902         section2 = match.group(2)
    903         section3 = match.group(3)
    904         end_slash = match.group(4)
    905 
    906         # Don't linkify things ending in slash (urls, for example)
    907         if end_slash:
    908           return whole_match
    909 
    910         candidate = ""
    911 
    912         # First try a two-level match
    913         candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2)
    914         got_two_level = False
    915 
    916         node = metadata.find_first(name_match(candidate2.replace('\n','')))
    917         if not node and '\n' in section2:
    918           # Linefeeds are ambiguous - was the intent to add a space,
    919           # or continue a lengthy name? Try the former now.
    920           candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')])
    921           node = metadata.find_first(name_match(candidate2b))
    922           if node:
    923             candidate2 = candidate2b
    924 
    925         if node:
    926           # Have two-level match
    927           got_two_level = True
    928           candidate = candidate2
    929         elif section3:
    930           # Try three-level match
    931           candidate3 = "%s%s" % (candidate2, section3)
    932           node = metadata.find_first(name_match(candidate3.replace('\n','')))
    933 
    934           if not node and '\n' in section3:
    935             # Linefeeds are ambiguous - was the intent to add a space,
    936             # or continue a lengthy name? Try the former now.
    937             candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')])
    938             node = metadata.find_first(name_match(candidate3b))
    939             if node:
    940               candidate3 = candidate3b
    941 
    942           if node:
    943             # Have 3-level match
    944             candidate = candidate3
    945 
    946         # Replace match with crossref or complain if a likely match couldn't be matched
    947 
    948         if node:
    949           tag_set.add(node)
    950           return whole_match.replace(candidate,filter_function(node))
    951         else:
    952           print >> sys.stderr,\
    953             "  WARNING: Could not crossref likely reference {%s}" % (match.group(0))
    954           return whole_match
    955 
    956       text = re.sub(tag_match, filter_sub, text)
    957 
    958     if summary_function is not None:
    959       return text + summary_function(sorted(tag_set, key=lambda x: x.name))
    960     else:
    961       return text
    962 
    963 def filter_links(text, filter_function, summary_function = None):
    964     """
    965     Find all references to tags in the form {@link xxx#yyy [zzz]} in the
    966     provided text, and pass them through filter_function and
    967     summary_function.
    968 
    969     Used to linkify documentation cross-references in HMTL, javadoc output.
    970 
    971     Args:
    972       text: A string representing a block of text destined for output
    973       metadata: A Metadata instance, the root of the metadata properties tree
    974       filter_function: A (string, string)->string function to apply to each 'xxx#yyy',
    975         zzz pair when found in text; the string returned replaces the tag name in text.
    976       summary_function: A string list->string function that is provided the list of
    977         unique targets found in text, and which must return a string that is
    978         then appended to the end of the text. The list is sorted alphabetically
    979         by node name.
    980 
    981     """
    982 
    983     target_set = set()
    984     def name_match(name):
    985       return lambda node: node.name == name
    986 
    987     tag_match = r"\{@link\s+([^\s\}]+)([^\}]*)\}"
    988 
    989     def filter_sub(match):
    990       whole_match = match.group(0)
    991       target = match.group(1)
    992       shortname = match.group(2).strip()
    993 
    994       #print "Found link '%s' as '%s' -> '%s'" % (target, shortname, filter_function(target, shortname))
    995 
    996       # Replace match with crossref
    997       target_set.add(target)
    998       return filter_function(target, shortname)
    999 
   1000     text = re.sub(tag_match, filter_sub, text)
   1001 
   1002     if summary_function is not None:
   1003       return text + summary_function(sorted(target_set))
   1004     else:
   1005       return text
   1006 
   1007 def any_visible(section, kind_name, visibilities):
   1008   """
   1009   Determine if entries in this section have an applied visibility that's in
   1010   the list of given visibilities.
   1011 
   1012   Args:
   1013     section: A section of metadata
   1014     kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
   1015     visibilities: An iterable of visibilities to match against
   1016 
   1017   Returns:
   1018     True if the section has any entries with any of the given visibilities. False otherwise.
   1019   """
   1020 
   1021   for inner_namespace in get_children_by_filtering_kind(section, kind_name,
   1022                                                         'namespaces'):
   1023     if any(filter_visibility(inner_namespace.merged_entries, visibilities)):
   1024       return True
   1025 
   1026   return any(filter_visibility(get_children_by_filtering_kind(section, kind_name,
   1027                                                               'merged_entries'),
   1028                                visibilities))
   1029 
   1030 
   1031 def filter_visibility(entries, visibilities):
   1032   """
   1033   Remove entries whose applied visibility is not in the supplied visibilities.
   1034 
   1035   Args:
   1036     entries: An iterable of Entry nodes
   1037     visibilities: An iterable of visibilities to filter against
   1038 
   1039   Yields:
   1040     An iterable of Entry nodes
   1041   """
   1042   return (e for e in entries if e.applied_visibility in visibilities)
   1043 
   1044 def remove_synthetic(entries):
   1045   """
   1046   Filter the given entries by removing those that are synthetic.
   1047 
   1048   Args:
   1049     entries: An iterable of Entry nodes
   1050 
   1051   Yields:
   1052     An iterable of Entry nodes
   1053   """
   1054   return (e for e in entries if not e.synthetic)
   1055 
   1056 def wbr(text):
   1057   """
   1058   Insert word break hints for the browser in the form of <wbr> HTML tags.
   1059 
   1060   Word breaks are inserted inside an HTML node only, so the nodes themselves
   1061   will not be changed. Attributes are also left unchanged.
   1062 
   1063   The following rules apply to insert word breaks:
   1064   - For characters in [ '.', '/', '_' ]
   1065   - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
   1066 
   1067   Args:
   1068     text: A string of text containing HTML content.
   1069 
   1070   Returns:
   1071     A string with <wbr> inserted by the above rules.
   1072   """
   1073   SPLIT_CHARS_LIST = ['.', '_', '/']
   1074   SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
   1075   CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
   1076   def wbr_filter(text):
   1077       new_txt = text
   1078 
   1079       # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
   1080       # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
   1081       for words in text.split(" "):
   1082         for char in SPLIT_CHARS_LIST:
   1083           # match at least x.y.z, don't match x or x.y
   1084           if len(words.split(char)) >= CAP_LETTER_MIN:
   1085             new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
   1086             new_txt = new_txt.replace(words, new_word)
   1087 
   1088       # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
   1089       new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
   1090 
   1091       return new_txt
   1092 
   1093   # Do not mangle HTML when doing the replace by using BeatifulSoup
   1094   # - Use the 'html.parser' to avoid inserting <html><body> when decoding
   1095   soup = bs4.BeautifulSoup(text, features='html.parser')
   1096   wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
   1097 
   1098   for navigable_string in soup.findAll(text=True):
   1099       parent = navigable_string.parent
   1100 
   1101       # Insert each '$text<wbr>$foo' before the old '$text$foo'
   1102       split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
   1103       for (split_string, last) in enumerate_with_last(split_by_wbr_list):
   1104           navigable_string.insert_before(split_string)
   1105 
   1106           if not last:
   1107             # Note that 'insert' will move existing tags to this spot
   1108             # so make a new tag instead
   1109             navigable_string.insert_before(wbr_tag())
   1110 
   1111       # Remove the old unmodified text
   1112       navigable_string.extract()
   1113 
   1114   return soup.decode()
   1115