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    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
947def 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
972def 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
1024def 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
1119def 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
1141def 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
1185def 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
1209def 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
1222def 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
1234def 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
1246def 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