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    # Crossref tag names
743    kind_mapping = {
744        'static': 'CameraCharacteristics',
745        'dynamic': 'CaptureResult',
746        'controls': 'CaptureRequest' }
747
748    # Convert metadata entry "android.x.y.z" to form
749    # "{@link CaptureRequest#X_Y_Z android.x.y.z}"
750    def javadoc_crossref_filter(node):
751      if node.applied_visibility == 'public':
752        return '{@link %s#%s %s}' % (kind_mapping[node.kind],
753                                     jkey_identifier(node.name),
754                                     node.name)
755      else:
756        return node.name
757
758    # For each public tag "android.x.y.z" referenced, add a
759    # "@see CaptureRequest#X_Y_Z"
760    def javadoc_see_filter(node_set):
761      node_set = (x for x in node_set if x.applied_visibility == 'public')
762
763      text = '\n'
764      for node in node_set:
765        text = text + '\n@see %s#%s' % (kind_mapping[node.kind],
766                                      jkey_identifier(node.name))
767
768      return text if text != '\n' else ''
769
770    javatext = filter_tags(javatext, metadata, javadoc_crossref_filter, javadoc_see_filter)
771
772    def line_filter(line):
773      # Indent each line
774      # Add ' * ' to it for stylistic reasons
775      # Strip right side of trailing whitespace
776      return (comment_prefix + line).rstrip()
777
778    # Process each line with above filter
779    javatext = "\n".join(line_filter(i) for i in javatext.split("\n")) + "\n"
780
781    return javatext
782
783  return javadoc_formatter
784
785def dedent(text):
786  """
787  Remove all common indentation from every line but the 0th.
788  This will avoid getting <code> blocks when rendering text via markdown.
789  Ignoring the 0th line will also allow the 0th line not to be aligned.
790
791  Args:
792    text: A string of text to dedent.
793
794  Returns:
795    String dedented by above rules.
796
797  For example:
798    assertEquals("bar\nline1\nline2",   dedent("bar\n  line1\n  line2"))
799    assertEquals("bar\nline1\nline2",   dedent(" bar\n  line1\n  line2"))
800    assertEquals("bar\n  line1\nline2", dedent(" bar\n    line1\n  line2"))
801  """
802  text = textwrap.dedent(text)
803  text_lines = text.split('\n')
804  text_not_first = "\n".join(text_lines[1:])
805  text_not_first = textwrap.dedent(text_not_first)
806  text = text_lines[0] + "\n" + text_not_first
807
808  return text
809
810def md(text, img_src_prefix=""):
811    """
812    Run text through markdown to produce HTML.
813
814    This also removes all common indentation from every line but the 0th.
815    This will avoid getting <code> blocks in markdown.
816    Ignoring the 0th line will also allow the 0th line not to be aligned.
817
818    Args:
819      text: A markdown-syntax using block of text to format.
820      img_src_prefix: An optional string to prepend to each <img src="target"/>
821
822    Returns:
823      String rendered by markdown and other rules applied (see above).
824
825    For example, this avoids the following situation:
826
827      <!-- Input -->
828
829      <!--- can't use dedent directly since 'foo' has no indent -->
830      <notes>foo
831          bar
832          bar
833      </notes>
834
835      <!-- Bad Output -- >
836      <!-- if no dedent is done generated code looks like -->
837      <p>foo
838        <code><pre>
839          bar
840          bar</pre></code>
841      </p>
842
843    Instead we get the more natural expected result:
844
845      <!-- Good Output -->
846      <p>foo
847      bar
848      bar</p>
849
850    """
851    text = dedent(text)
852
853    # full list of extensions at http://pythonhosted.org/Markdown/extensions/
854    md_extensions = ['tables'] # make <table> with ASCII |_| tables
855    # render with markdown
856    text = markdown.markdown(text, md_extensions)
857
858    # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo">
859    text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text)
860    return text
861
862def filter_tags(text, metadata, filter_function, summary_function = None):
863    """
864    Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in
865    the provided text, and pass them through filter_function and summary_function.
866
867    Used to linkify entry names in HMTL, javadoc output.
868
869    Args:
870      text: A string representing a block of text destined for output
871      metadata: A Metadata instance, the root of the metadata properties tree
872      filter_function: A Node->string function to apply to each node
873        when found in text; the string returned replaces the tag name in text.
874      summary_function: A Node list->string function that is provided the list of
875        unique tag nodes found in text, and which must return a string that is
876        then appended to the end of the text. The list is sorted alphabetically
877        by node name.
878    """
879
880    tag_set = set()
881    def name_match(name):
882      return lambda node: node.name == name
883
884    # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure
885    # to grab .z and not just outer_namespace.x.y.  (sloppy, but since we
886    # check for validity, a few false positives don't hurt)
887    for outer_namespace in metadata.outer_namespaces:
888
889      tag_match = outer_namespace.name + \
890        r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)"
891
892      def filter_sub(match):
893        whole_match = match.group(0)
894        section1 = match.group(1)
895        section2 = match.group(2)
896        section3 = match.group(3)
897        end_slash = match.group(4)
898
899        # Don't linkify things ending in slash (urls, for example)
900        if end_slash:
901          return whole_match
902
903        candidate = ""
904
905        # First try a two-level match
906        candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2)
907        got_two_level = False
908
909        node = metadata.find_first(name_match(candidate2.replace('\n','')))
910        if not node and '\n' in section2:
911          # Linefeeds are ambiguous - was the intent to add a space,
912          # or continue a lengthy name? Try the former now.
913          candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')])
914          node = metadata.find_first(name_match(candidate2b))
915          if node:
916            candidate2 = candidate2b
917
918        if node:
919          # Have two-level match
920          got_two_level = True
921          candidate = candidate2
922        elif section3:
923          # Try three-level match
924          candidate3 = "%s%s" % (candidate2, section3)
925          node = metadata.find_first(name_match(candidate3.replace('\n','')))
926
927          if not node and '\n' in section3:
928            # Linefeeds are ambiguous - was the intent to add a space,
929            # or continue a lengthy name? Try the former now.
930            candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')])
931            node = metadata.find_first(name_match(candidate3b))
932            if node:
933              candidate3 = candidate3b
934
935          if node:
936            # Have 3-level match
937            candidate = candidate3
938
939        # Replace match with crossref or complain if a likely match couldn't be matched
940
941        if node:
942          tag_set.add(node)
943          return whole_match.replace(candidate,filter_function(node))
944        else:
945          print >> sys.stderr,\
946            "  WARNING: Could not crossref likely reference {%s}" % (match.group(0))
947          return whole_match
948
949      text = re.sub(tag_match, filter_sub, text)
950
951    if summary_function is not None:
952      return text + summary_function(sorted(tag_set, key=lambda x: x.name))
953    else:
954      return text
955
956def any_visible(section, kind_name, visibilities):
957  """
958  Determine if entries in this section have an applied visibility that's in
959  the list of given visibilities.
960
961  Args:
962    section: A section of metadata
963    kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
964    visibilities: An iterable of visibilities to match against
965
966  Returns:
967    True if the section has any entries with any of the given visibilities. False otherwise.
968  """
969
970  for inner_namespace in get_children_by_filtering_kind(section, kind_name,
971                                                        'namespaces'):
972    if any(filter_visibility(inner_namespace.merged_entries, visibilities)):
973      return True
974
975  return any(filter_visibility(get_children_by_filtering_kind(section, kind_name,
976                                                              'merged_entries'),
977                               visibilities))
978
979
980def filter_visibility(entries, visibilities):
981  """
982  Remove entries whose applied visibility is not in the supplied visibilities.
983
984  Args:
985    entries: An iterable of Entry nodes
986    visibilities: An iterable of visibilities to filter against
987
988  Yields:
989    An iterable of Entry nodes
990  """
991  return (e for e in entries if e.applied_visibility in visibilities)
992
993def remove_synthetic(entries):
994  """
995  Filter the given entries by removing those that are synthetic.
996
997  Args:
998    entries: An iterable of Entry nodes
999
1000  Yields:
1001    An iterable of Entry nodes
1002  """
1003  return (e for e in entries if not e.synthetic)
1004
1005def wbr(text):
1006  """
1007  Insert word break hints for the browser in the form of <wbr> HTML tags.
1008
1009  Word breaks are inserted inside an HTML node only, so the nodes themselves
1010  will not be changed. Attributes are also left unchanged.
1011
1012  The following rules apply to insert word breaks:
1013  - For characters in [ '.', '/', '_' ]
1014  - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
1015
1016  Args:
1017    text: A string of text containing HTML content.
1018
1019  Returns:
1020    A string with <wbr> inserted by the above rules.
1021  """
1022  SPLIT_CHARS_LIST = ['.', '_', '/']
1023  SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
1024  CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
1025  def wbr_filter(text):
1026      new_txt = text
1027
1028      # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
1029      # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
1030      for words in text.split(" "):
1031        for char in SPLIT_CHARS_LIST:
1032          # match at least x.y.z, don't match x or x.y
1033          if len(words.split(char)) >= CAP_LETTER_MIN:
1034            new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
1035            new_txt = new_txt.replace(words, new_word)
1036
1037      # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
1038      new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
1039
1040      return new_txt
1041
1042  # Do not mangle HTML when doing the replace by using BeatifulSoup
1043  # - Use the 'html.parser' to avoid inserting <html><body> when decoding
1044  soup = bs4.BeautifulSoup(text, features='html.parser')
1045  wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
1046
1047  for navigable_string in soup.findAll(text=True):
1048      parent = navigable_string.parent
1049
1050      # Insert each '$text<wbr>$foo' before the old '$text$foo'
1051      split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
1052      for (split_string, last) in enumerate_with_last(split_by_wbr_list):
1053          navigable_string.insert_before(split_string)
1054
1055          if not last:
1056            # Note that 'insert' will move existing tags to this spot
1057            # so make a new tag instead
1058            navigable_string.insert_before(wbr_tag())
1059
1060      # Remove the old unmodified text
1061      navigable_string.extract()
1062
1063  return soup.decode()
1064