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     ndktext = md(text, NDKDOC_IMAGE_SRC_METADATA, False)
    908 
    909     # Convert metadata entry "android.x.y.z" to form
    910     # NDK tag format of "ACAMERA_X_Y_Z"
    911     def ndkdoc_crossref_filter(node):
    912       if node.applied_ndk_visible == 'true':
    913         return csym(ndk(node.name))
    914       else:
    915         return node.name
    916 
    917     # For each public tag "android.x.y.z" referenced, add a
    918     # "@see ACAMERA_X_Y_Z"
    919     def ndkdoc_crossref_see_filter(node_set):
    920       node_set = (x for x in node_set if x.applied_ndk_visible == 'true')
    921 
    922       text = '\n'
    923       for node in node_set:
    924         text = text + '\n@see %s' % (csym(ndk(node.name)))
    925 
    926       return text if text != '\n' else ''
    927 
    928     ndktext = filter_tags(ndktext, metadata, ndkdoc_crossref_filter, ndkdoc_crossref_see_filter)
    929 
    930     ndktext = ndk_replace_tag_wildcards(ndktext, metadata)
    931 
    932     comment_prefix = " " * indent + " * ";
    933 
    934     def line_filter(line):
    935       # Indent each line
    936       # Add ' * ' to it for stylistic reasons
    937       # Strip right side of trailing whitespace
    938       return (comment_prefix + line).rstrip()
    939 
    940     # Process each line with above filter
    941     ndktext = "\n".join(line_filter(i) for i in ndktext.split("\n")) + "\n"
    942 
    943     return ndktext
    944 
    945   return ndkdoc_formatter
    946 
    947 def dedent(text):
    948   """
    949   Remove all common indentation from every line but the 0th.
    950   This will avoid getting <code> blocks when rendering text via markdown.
    951   Ignoring the 0th line will also allow the 0th line not to be aligned.
    952 
    953   Args:
    954     text: A string of text to dedent.
    955 
    956   Returns:
    957     String dedented by above rules.
    958 
    959   For example:
    960     assertEquals("bar\nline1\nline2",   dedent("bar\n  line1\n  line2"))
    961     assertEquals("bar\nline1\nline2",   dedent(" bar\n  line1\n  line2"))
    962     assertEquals("bar\n  line1\nline2", dedent(" bar\n    line1\n  line2"))
    963   """
    964   text = textwrap.dedent(text)
    965   text_lines = text.split('\n')
    966   text_not_first = "\n".join(text_lines[1:])
    967   text_not_first = textwrap.dedent(text_not_first)
    968   text = text_lines[0] + "\n" + text_not_first
    969 
    970   return text
    971 
    972 def md(text, img_src_prefix="", table_ext=True):
    973     """
    974     Run text through markdown to produce HTML.
    975 
    976     This also removes all common indentation from every line but the 0th.
    977     This will avoid getting <code> blocks in markdown.
    978     Ignoring the 0th line will also allow the 0th line not to be aligned.
    979 
    980     Args:
    981       text: A markdown-syntax using block of text to format.
    982       img_src_prefix: An optional string to prepend to each <img src="target"/>
    983 
    984     Returns:
    985       String rendered by markdown and other rules applied (see above).
    986 
    987     For example, this avoids the following situation:
    988 
    989       <!-- Input -->
    990 
    991       <!--- can't use dedent directly since 'foo' has no indent -->
    992       <notes>foo
    993           bar
    994           bar
    995       </notes>
    996 
    997       <!-- Bad Output -- >
    998       <!-- if no dedent is done generated code looks like -->
    999       <p>foo
   1000         <code><pre>
   1001           bar
   1002           bar</pre></code>
   1003       </p>
   1004 
   1005     Instead we get the more natural expected result:
   1006 
   1007       <!-- Good Output -->
   1008       <p>foo
   1009       bar
   1010       bar</p>
   1011 
   1012     """
   1013     text = dedent(text)
   1014 
   1015     # full list of extensions at http://pythonhosted.org/Markdown/extensions/
   1016     md_extensions = ['tables'] if table_ext else []# make <table> with ASCII |_| tables
   1017     # render with markdown
   1018     text = markdown.markdown(text, md_extensions)
   1019 
   1020     # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo">
   1021     text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text)
   1022     return text
   1023 
   1024 def filter_tags(text, metadata, filter_function, summary_function = None):
   1025     """
   1026     Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in
   1027     the provided text, and pass them through filter_function and summary_function.
   1028 
   1029     Used to linkify entry names in HMTL, javadoc output.
   1030 
   1031     Args:
   1032       text: A string representing a block of text destined for output
   1033       metadata: A Metadata instance, the root of the metadata properties tree
   1034       filter_function: A Node->string function to apply to each node
   1035         when found in text; the string returned replaces the tag name in text.
   1036       summary_function: A Node list->string function that is provided the list of
   1037         unique tag nodes found in text, and which must return a string that is
   1038         then appended to the end of the text. The list is sorted alphabetically
   1039         by node name.
   1040     """
   1041 
   1042     tag_set = set()
   1043     def name_match(name):
   1044       return lambda node: node.name == name
   1045 
   1046     # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure
   1047     # to grab .z and not just outer_namespace.x.y.  (sloppy, but since we
   1048     # check for validity, a few false positives don't hurt).
   1049     # Try to ignore items of the form {@link <outer_namespace>...
   1050     for outer_namespace in metadata.outer_namespaces:
   1051 
   1052       tag_match = r"(?<!\{@link\s)" + outer_namespace.name + \
   1053         r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)"
   1054 
   1055       def filter_sub(match):
   1056         whole_match = match.group(0)
   1057         section1 = match.group(1)
   1058         section2 = match.group(2)
   1059         section3 = match.group(3)
   1060         end_slash = match.group(4)
   1061 
   1062         # Don't linkify things ending in slash (urls, for example)
   1063         if end_slash:
   1064           return whole_match
   1065 
   1066         candidate = ""
   1067 
   1068         # First try a two-level match
   1069         candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2)
   1070         got_two_level = False
   1071 
   1072         node = metadata.find_first(name_match(candidate2.replace('\n','')))
   1073         if not node and '\n' in section2:
   1074           # Linefeeds are ambiguous - was the intent to add a space,
   1075           # or continue a lengthy name? Try the former now.
   1076           candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')])
   1077           node = metadata.find_first(name_match(candidate2b))
   1078           if node:
   1079             candidate2 = candidate2b
   1080 
   1081         if node:
   1082           # Have two-level match
   1083           got_two_level = True
   1084           candidate = candidate2
   1085         elif section3:
   1086           # Try three-level match
   1087           candidate3 = "%s%s" % (candidate2, section3)
   1088           node = metadata.find_first(name_match(candidate3.replace('\n','')))
   1089 
   1090           if not node and '\n' in section3:
   1091             # Linefeeds are ambiguous - was the intent to add a space,
   1092             # or continue a lengthy name? Try the former now.
   1093             candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')])
   1094             node = metadata.find_first(name_match(candidate3b))
   1095             if node:
   1096               candidate3 = candidate3b
   1097 
   1098           if node:
   1099             # Have 3-level match
   1100             candidate = candidate3
   1101 
   1102         # Replace match with crossref or complain if a likely match couldn't be matched
   1103 
   1104         if node:
   1105           tag_set.add(node)
   1106           return whole_match.replace(candidate,filter_function(node))
   1107         else:
   1108           print >> sys.stderr,\
   1109             "  WARNING: Could not crossref likely reference {%s}" % (match.group(0))
   1110           return whole_match
   1111 
   1112       text = re.sub(tag_match, filter_sub, text)
   1113 
   1114     if summary_function is not None:
   1115       return text + summary_function(sorted(tag_set, key=lambda x: x.name))
   1116     else:
   1117       return text
   1118 
   1119 def ndk_replace_tag_wildcards(text, metadata):
   1120     """
   1121     Find all references to tags in the form android.xxx.* or android.xxx.yyy.*
   1122     in the provided text, and replace them by NDK format of "ACAMERA_XXX_*" or
   1123     "ACAMERA_XXX_YYY_*"
   1124 
   1125     Args:
   1126       text: A string representing a block of text destined for output
   1127       metadata: A Metadata instance, the root of the metadata properties tree
   1128     """
   1129     tag_match = r"android\.([a-zA-Z0-9\n]+)\.\*"
   1130     tag_match_2 = r"android\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)\*"
   1131 
   1132     def filter_sub(match):
   1133       return "ACAMERA_" + match.group(1).upper() + "_*"
   1134     def filter_sub_2(match):
   1135       return "ACAMERA_" + match.group(1).upper() + match.group(2).upper() + "_*"
   1136 
   1137     text = re.sub(tag_match, filter_sub, text)
   1138     text = re.sub(tag_match_2, filter_sub_2, text)
   1139     return text
   1140 
   1141 def filter_links(text, filter_function, summary_function = None):
   1142     """
   1143     Find all references to tags in the form {@link xxx#yyy [zzz]} in the
   1144     provided text, and pass them through filter_function and
   1145     summary_function.
   1146 
   1147     Used to linkify documentation cross-references in HMTL, javadoc output.
   1148 
   1149     Args:
   1150       text: A string representing a block of text destined for output
   1151       metadata: A Metadata instance, the root of the metadata properties tree
   1152       filter_function: A (string, string)->string function to apply to each 'xxx#yyy',
   1153         zzz pair when found in text; the string returned replaces the tag name in text.
   1154       summary_function: A string list->string function that is provided the list of
   1155         unique targets found in text, and which must return a string that is
   1156         then appended to the end of the text. The list is sorted alphabetically
   1157         by node name.
   1158 
   1159     """
   1160 
   1161     target_set = set()
   1162     def name_match(name):
   1163       return lambda node: node.name == name
   1164 
   1165     tag_match = r"\{@link\s+([^\s\}]+)([^\}]*)\}"
   1166 
   1167     def filter_sub(match):
   1168       whole_match = match.group(0)
   1169       target = match.group(1)
   1170       shortname = match.group(2).strip()
   1171 
   1172       #print "Found link '%s' as '%s' -> '%s'" % (target, shortname, filter_function(target, shortname))
   1173 
   1174       # Replace match with crossref
   1175       target_set.add(target)
   1176       return filter_function(target, shortname)
   1177 
   1178     text = re.sub(tag_match, filter_sub, text)
   1179 
   1180     if summary_function is not None:
   1181       return text + summary_function(sorted(target_set))
   1182     else:
   1183       return text
   1184 
   1185 def any_visible(section, kind_name, visibilities):
   1186   """
   1187   Determine if entries in this section have an applied visibility that's in
   1188   the list of given visibilities.
   1189 
   1190   Args:
   1191     section: A section of metadata
   1192     kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
   1193     visibilities: An iterable of visibilities to match against
   1194 
   1195   Returns:
   1196     True if the section has any entries with any of the given visibilities. False otherwise.
   1197   """
   1198 
   1199   for inner_namespace in get_children_by_filtering_kind(section, kind_name,
   1200                                                         'namespaces'):
   1201     if any(filter_visibility(inner_namespace.merged_entries, visibilities)):
   1202       return True
   1203 
   1204   return any(filter_visibility(get_children_by_filtering_kind(section, kind_name,
   1205                                                               'merged_entries'),
   1206                                visibilities))
   1207 
   1208 
   1209 def filter_visibility(entries, visibilities):
   1210   """
   1211   Remove entries whose applied visibility is not in the supplied visibilities.
   1212 
   1213   Args:
   1214     entries: An iterable of Entry nodes
   1215     visibilities: An iterable of visibilities to filter against
   1216 
   1217   Yields:
   1218     An iterable of Entry nodes
   1219   """
   1220   return (e for e in entries if e.applied_visibility in visibilities)
   1221 
   1222 def remove_synthetic(entries):
   1223   """
   1224   Filter the given entries by removing those that are synthetic.
   1225 
   1226   Args:
   1227     entries: An iterable of Entry nodes
   1228 
   1229   Yields:
   1230     An iterable of Entry nodes
   1231   """
   1232   return (e for e in entries if not e.synthetic)
   1233 
   1234 def filter_ndk_visible(entries):
   1235   """
   1236   Filter the given entries by removing those that are not NDK visible.
   1237 
   1238   Args:
   1239     entries: An iterable of Entry nodes
   1240 
   1241   Yields:
   1242     An iterable of Entry nodes
   1243   """
   1244   return (e for e in entries if e.applied_ndk_visible == 'true')
   1245 
   1246 def wbr(text):
   1247   """
   1248   Insert word break hints for the browser in the form of <wbr> HTML tags.
   1249 
   1250   Word breaks are inserted inside an HTML node only, so the nodes themselves
   1251   will not be changed. Attributes are also left unchanged.
   1252 
   1253   The following rules apply to insert word breaks:
   1254   - For characters in [ '.', '/', '_' ]
   1255   - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
   1256 
   1257   Args:
   1258     text: A string of text containing HTML content.
   1259 
   1260   Returns:
   1261     A string with <wbr> inserted by the above rules.
   1262   """
   1263   SPLIT_CHARS_LIST = ['.', '_', '/']
   1264   SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
   1265   CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
   1266   def wbr_filter(text):
   1267       new_txt = text
   1268 
   1269       # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
   1270       # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
   1271       for words in text.split(" "):
   1272         for char in SPLIT_CHARS_LIST:
   1273           # match at least x.y.z, don't match x or x.y
   1274           if len(words.split(char)) >= CAP_LETTER_MIN:
   1275             new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
   1276             new_txt = new_txt.replace(words, new_word)
   1277 
   1278       # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
   1279       new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
   1280 
   1281       return new_txt
   1282 
   1283   # Do not mangle HTML when doing the replace by using BeatifulSoup
   1284   # - Use the 'html.parser' to avoid inserting <html><body> when decoding
   1285   soup = bs4.BeautifulSoup(text, features='html.parser')
   1286   wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
   1287 
   1288   for navigable_string in soup.findAll(text=True):
   1289       parent = navigable_string.parent
   1290 
   1291       # Insert each '$text<wbr>$foo' before the old '$text$foo'
   1292       split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
   1293       for (split_string, last) in enumerate_with_last(split_by_wbr_list):
   1294           navigable_string.insert_before(split_string)
   1295 
   1296           if not last:
   1297             # Note that 'insert' will move existing tags to this spot
   1298             # so make a new tag instead
   1299             navigable_string.insert_before(wbr_tag())
   1300 
   1301       # Remove the old unmodified text
   1302       navigable_string.extract()
   1303 
   1304   return soup.decode()
   1305