• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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"""
18A set of helpers for rendering Mako templates with a Metadata model.
19"""
20
21import metadata_model
22import re
23import markdown
24import textwrap
25import sys
26import bs4
27# Monkey-patch BS4. WBR element must not have an end tag.
28bs4.builder.HTMLTreeBuilder.empty_element_tags.add("wbr")
29
30from collections import OrderedDict
31
32# Relative path from HTML file to the base directory used by <img> tags
33IMAGE_SRC_METADATA="images/camera2/metadata/"
34
35# Prepend this path to each <img src="foo"> in javadocs
36JAVADOC_IMAGE_SRC_METADATA="../../../../" + IMAGE_SRC_METADATA
37NDKDOC_IMAGE_SRC_METADATA="../" + IMAGE_SRC_METADATA
38
39_context_buf = None
40
41def _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
49def 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
64def 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)
77def 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
107def 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
131def 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
147def 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
202def 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
211def 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
224def 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
251def 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
275def 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
291def 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
304def 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
331def _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
349def 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
378def 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
401def _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.
428def _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
440def 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
493def 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
511def _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
532def _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
557def 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
577def 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
594def 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
609def 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
660def 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
687def 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
710def 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
728def 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
743def 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
783def 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
872def 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
948def 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
973def 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
1025def 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
1120def 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
1142def 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
1186def 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
1210def 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
1223def 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
1235def 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
1247def 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