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