OSDN Git Service

a501524b5ad8d10f49389fd2fe965b9815f20b58
[android-x86/system-media.git] / camera / docs / metadata_helpers.py
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()