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