metadata_helpers.py revision 88b858d5e4db3eb66fe570647626a592ebb6af91
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:
386      # Always map 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 _jtype_primitive(what):
431  """
432  Calculate the Java type from an entry type string.
433
434  Remarks:
435    Makes a special exception for Rational, since it's a primitive in terms of
436    the C-library camera_metadata type system.
437
438  Returns:
439    The string representing the primitive type
440  """
441  mapping = {
442    'int32': 'int',
443    'int64': 'long',
444    'float': 'float',
445    'double': 'double',
446    'byte': 'byte',
447    'rational': 'Rational'
448  }
449
450  try:
451    return mapping[what]
452  except KeyError as e:
453    raise ValueError("Can't map '%s' to a primitive, not supported" %what)
454
455def jclass(entry):
456  """
457  Calculate the java Class reference string for an entry.
458
459  Args:
460    entry: an Entry node
461
462  Example:
463    <entry name="some_int" type="int32"/>
464    <entry name="some_int_array" type="int32" container='array'/>
465
466    jclass(some_int) == 'int.class'
467    jclass(some_int_array) == 'int[].class'
468
469  Returns:
470    The ClassName.class string
471  """
472
473  return "%s.class" %jtype_unboxed(entry)
474
475def jidentifier(what):
476  """
477  Convert the input string into a valid Java identifier.
478
479  Args:
480    what: any identifier string
481
482  Returns:
483    String with added underscores if necessary.
484  """
485  if re.match("\d", what):
486    return "_%s" %what
487  else:
488    return what
489
490def enum_calculate_value_string(enum_value):
491  """
492  Calculate the value of the enum, even if it does not have one explicitly
493  defined.
494
495  This looks back for the first enum value that has a predefined value and then
496  applies addition until we get the right value, using C-enum semantics.
497
498  Args:
499    enum_value: an EnumValue node with a valid Enum parent
500
501  Example:
502    <enum>
503      <value>X</value>
504      <value id="5">Y</value>
505      <value>Z</value>
506    </enum>
507
508    enum_calculate_value_string(X) == '0'
509    enum_calculate_Value_string(Y) == '5'
510    enum_calculate_value_string(Z) == '6'
511
512  Returns:
513    String that represents the enum value as an integer literal.
514  """
515
516  enum_value_siblings = list(enum_value.parent.values)
517  this_index = enum_value_siblings.index(enum_value)
518
519  def is_hex_string(instr):
520    return bool(re.match('0x[a-f0-9]+$', instr, re.IGNORECASE))
521
522  base_value = 0
523  base_offset = 0
524  emit_as_hex = False
525
526  this_id = enum_value_siblings[this_index].id
527  while this_index != 0 and not this_id:
528    this_index -= 1
529    base_offset += 1
530    this_id = enum_value_siblings[this_index].id
531
532  if this_id:
533    base_value = int(this_id, 0)  # guess base
534    emit_as_hex = is_hex_string(this_id)
535
536  if emit_as_hex:
537    return "0x%X" %(base_value + base_offset)
538  else:
539    return "%d" %(base_value + base_offset)
540
541def enumerate_with_last(iterable):
542  """
543  Enumerate a sequence of iterable, while knowing if this element is the last in
544  the sequence or not.
545
546  Args:
547    iterable: an Iterable of some sequence
548
549  Yields:
550    (element, bool) where the bool is True iff the element is last in the seq.
551  """
552  it = (i for i in iterable)
553
554  first = next(it)  # OK: raises exception if it is empty
555
556  second = first  # for when we have only 1 element in iterable
557
558  try:
559    while True:
560      second = next(it)
561      # more elements remaining.
562      yield (first, False)
563      first = second
564  except StopIteration:
565    # last element. no more elements left
566    yield (second, True)
567
568def pascal_case(what):
569  """
570  Convert the first letter of a string to uppercase, to make the identifier
571  conform to PascalCase.
572
573  If there are dots, remove the dots, and capitalize the letter following
574  where the dot was. Letters that weren't following dots are left unchanged,
575  except for the first letter of the string (which is made upper-case).
576
577  Args:
578    what: a string representing some identifier
579
580  Returns:
581    String with first letter capitalized
582
583  Example:
584    pascal_case("helloWorld") == "HelloWorld"
585    pascal_case("foo") == "Foo"
586    pascal_case("hello.world") = "HelloWorld"
587    pascal_case("fooBar.fooBar") = "FooBarFooBar"
588  """
589  return "".join([s[0:1].upper() + s[1:] for s in what.split('.')])
590
591def jkey_identifier(what):
592  """
593  Return a Java identifier from a property name.
594
595  Args:
596    what: a string representing a property name.
597
598  Returns:
599    Java identifier corresponding to the property name. May need to be
600    prepended with the appropriate Java class name by the caller of this
601    function. Note that the outer namespace is stripped from the property
602    name.
603
604  Example:
605    jkey_identifier("android.lens.facing") == "LENS_FACING"
606  """
607  return csym(what[what.find('.') + 1:])
608
609def jenum_value(enum_entry, enum_value):
610  """
611  Calculate the Java name for an integer enum value
612
613  Args:
614    enum: An enum-typed Entry node
615    value: An EnumValue node for the enum
616
617  Returns:
618    String representing the Java symbol
619  """
620
621  cname = csym(enum_entry.name)
622  return cname[cname.find('_') + 1:] + '_' + enum_value.name
623
624def javadoc(metadata, indent = 4):
625  """
626  Returns a function to format a markdown syntax text block as a
627  javadoc comment section, given a set of metadata
628
629  Args:
630    metadata: A Metadata instance, representing the the top-level root
631      of the metadata for cross-referencing
632    indent: baseline level of indentation for javadoc block
633  Returns:
634    A function that transforms a String text block as follows:
635    - Indent and * for insertion into a Javadoc comment block
636    - Trailing whitespace removed
637    - Entire body rendered via markdown to generate HTML
638    - All tag names converted to appropriate Javadoc {@link} with @see
639      for each tag
640
641  Example:
642    "This is a comment for Javadoc\n" +
643    "     with multiple lines, that should be   \n" +
644    "     formatted better\n" +
645    "\n" +
646    "    That covers multiple lines as well\n"
647    "    And references android.control.mode\n"
648
649    transforms to
650    "    * <p>This is a comment for Javadoc\n" +
651    "    * with multiple lines, that should be\n" +
652    "    * formatted better</p>\n" +
653    "    * <p>That covers multiple lines as well</p>\n" +
654    "    * and references {@link CaptureRequest#CONTROL_MODE android.control.mode}\n" +
655    "    *\n" +
656    "    * @see CaptureRequest#CONTROL_MODE\n"
657  """
658  def javadoc_formatter(text):
659    comment_prefix = " " * indent + " * ";
660
661    # render with markdown => HTML
662    javatext = md(text, JAVADOC_IMAGE_SRC_METADATA)
663
664    # Crossref tag names
665    kind_mapping = {
666        'static': 'CameraCharacteristics',
667        'dynamic': 'CaptureResult',
668        'controls': 'CaptureRequest' }
669
670    # Convert metadata entry "android.x.y.z" to form
671    # "{@link CaptureRequest#X_Y_Z android.x.y.z}"
672    def javadoc_crossref_filter(node):
673      if node.applied_visibility == 'public':
674        return '{@link %s#%s %s}' % (kind_mapping[node.kind],
675                                     jkey_identifier(node.name),
676                                     node.name)
677      else:
678        return node.name
679
680    # For each public tag "android.x.y.z" referenced, add a
681    # "@see CaptureRequest#X_Y_Z"
682    def javadoc_see_filter(node_set):
683      node_set = (x for x in node_set if x.applied_visibility == 'public')
684
685      text = '\n'
686      for node in node_set:
687        text = text + '\n@see %s#%s' % (kind_mapping[node.kind],
688                                      jkey_identifier(node.name))
689
690      return text if text != '\n' else ''
691
692    javatext = filter_tags(javatext, metadata, javadoc_crossref_filter, javadoc_see_filter)
693
694    def line_filter(line):
695      # Indent each line
696      # Add ' * ' to it for stylistic reasons
697      # Strip right side of trailing whitespace
698      return (comment_prefix + line.lstrip()).rstrip()
699
700    # Process each line with above filter
701    javatext = "\n".join(line_filter(i) for i in javatext.split("\n")) + "\n"
702
703    return javatext
704
705  return javadoc_formatter
706
707def dedent(text):
708  """
709  Remove all common indentation from every line but the 0th.
710  This will avoid getting <code> blocks when rendering text via markdown.
711  Ignoring the 0th line will also allow the 0th line not to be aligned.
712
713  Args:
714    text: A string of text to dedent.
715
716  Returns:
717    String dedented by above rules.
718
719  For example:
720    assertEquals("bar\nline1\nline2",   dedent("bar\n  line1\n  line2"))
721    assertEquals("bar\nline1\nline2",   dedent(" bar\n  line1\n  line2"))
722    assertEquals("bar\n  line1\nline2", dedent(" bar\n    line1\n  line2"))
723  """
724  text = textwrap.dedent(text)
725  text_lines = text.split('\n')
726  text_not_first = "\n".join(text_lines[1:])
727  text_not_first = textwrap.dedent(text_not_first)
728  text = text_lines[0] + "\n" + text_not_first
729
730  return text
731
732def md(text, img_src_prefix=""):
733    """
734    Run text through markdown to produce HTML.
735
736    This also removes all common indentation from every line but the 0th.
737    This will avoid getting <code> blocks in markdown.
738    Ignoring the 0th line will also allow the 0th line not to be aligned.
739
740    Args:
741      text: A markdown-syntax using block of text to format.
742      img_src_prefix: An optional string to prepend to each <img src="target"/>
743
744    Returns:
745      String rendered by markdown and other rules applied (see above).
746
747    For example, this avoids the following situation:
748
749      <!-- Input -->
750
751      <!--- can't use dedent directly since 'foo' has no indent -->
752      <notes>foo
753          bar
754          bar
755      </notes>
756
757      <!-- Bad Output -- >
758      <!-- if no dedent is done generated code looks like -->
759      <p>foo
760        <code><pre>
761          bar
762          bar</pre></code>
763      </p>
764
765    Instead we get the more natural expected result:
766
767      <!-- Good Output -->
768      <p>foo
769      bar
770      bar</p>
771
772    """
773    text = dedent(text)
774
775    # full list of extensions at http://pythonhosted.org/Markdown/extensions/
776    md_extensions = ['tables'] # make <table> with ASCII |_| tables
777    # render with markdown
778    text = markdown.markdown(text, md_extensions)
779
780    # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo">
781    text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text)
782    return text
783
784def filter_tags(text, metadata, filter_function, summary_function = None):
785    """
786    Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in
787    the provided text, and pass them through filter_function and summary_function.
788
789    Used to linkify entry names in HMTL, javadoc output.
790
791    Args:
792      text: A string representing a block of text destined for output
793      metadata: A Metadata instance, the root of the metadata properties tree
794      filter_function: A Node->string function to apply to each node
795        when found in text; the string returned replaces the tag name in text.
796      summary_function: A Node set->string function that is provided the set of
797        tag nodes found in text, and which must return a string that is then appended
798        to the end of the text.
799    """
800
801    tag_set = set()
802    def name_match(name):
803      return lambda node: node.name == name
804
805    # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure
806    # to grab .z and not just outer_namespace.x.y.  (sloppy, but since we
807    # check for validity, a few false positives don't hurt)
808    for outer_namespace in metadata.outer_namespaces:
809
810      tag_match = outer_namespace.name + \
811        r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?[^/]"
812
813      def filter_sub(match):
814        whole_match = match.group(0)
815        section1 = match.group(1)
816        section2 = match.group(2)
817        section3 = match.group(3)
818
819        # First try a two-level match
820        candidate = "%s.%s.%s" % (outer_namespace.name, section1, section2)
821        got_two_level = False
822
823        node = metadata.find_first(name_match(candidate.replace('\n','')))
824
825        if node:
826          got_two_level = True
827
828        # Then a three-level match
829        if not got_two_level and section3:
830          candidate = "%s%s" % (candidate, section3)
831          node = metadata.find_first(name_match(candidate.replace('\n','')))
832
833        if node:
834          tag_set.add(node)
835          return whole_match.replace(candidate,filter_function(node))
836        else:
837          print >> sys.stderr,\
838            "  WARNING: Could not crossref likely reference {%s}" % (match.group(0))
839          return whole_match
840
841      text = re.sub(tag_match, filter_sub, text)
842
843    if summary_function is not None:
844      return text + summary_function(tag_set)
845    else:
846      return text
847
848def any_visible(section, kind_name, visibilities):
849  """
850  Determine if entries in this section have an applied visibility that's in
851  the list of given visibilities.
852
853  Args:
854    section: A section of metadata
855    kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
856    visibilities: An iterable of visibilities to match against
857
858  Returns:
859    True if the section has any entries with any of the given visibilities. False otherwise.
860  """
861
862  for inner_namespace in get_children_by_filtering_kind(section, kind_name,
863                                                        'namespaces'):
864    if any(filter_visibility(inner_namespace.merged_entries, visibilities)):
865      return True
866
867  return any(filter_visibility(get_children_by_filtering_kind(section, kind_name,
868                                                              'merged_entries'),
869                               visibilities))
870
871
872def filter_visibility(entries, visibilities):
873  """
874  Remove entries whose applied visibility is not in the supplied visibilities.
875
876  Args:
877    entries: An iterable of Entry nodes
878    visibilities: An iterable of visibilities to filter against
879
880  Yields:
881    An iterable of Entry nodes
882  """
883  return (e for e in entries if e.applied_visibility in visibilities)
884
885def wbr(text):
886  """
887  Insert word break hints for the browser in the form of <wbr> HTML tags.
888
889  Word breaks are inserted inside an HTML node only, so the nodes themselves
890  will not be changed. Attributes are also left unchanged.
891
892  The following rules apply to insert word breaks:
893  - For characters in [ '.', '/', '_' ]
894  - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
895
896  Args:
897    text: A string of text containing HTML content.
898
899  Returns:
900    A string with <wbr> inserted by the above rules.
901  """
902  SPLIT_CHARS_LIST = ['.', '_', '/']
903  SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
904  CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
905  def wbr_filter(text):
906      new_txt = text
907
908      # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
909      # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
910      for words in text.split(" "):
911        for char in SPLIT_CHARS_LIST:
912          # match at least x.y.z, don't match x or x.y
913          if len(words.split(char)) >= CAP_LETTER_MIN:
914            new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
915            new_txt = new_txt.replace(words, new_word)
916
917      # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
918      new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
919
920      return new_txt
921
922  # Do not mangle HTML when doing the replace by using BeatifulSoup
923  # - Use the 'html.parser' to avoid inserting <html><body> when decoding
924  soup = bs4.BeautifulSoup(text, features='html.parser')
925  wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
926
927  for navigable_string in soup.findAll(text=True):
928      parent = navigable_string.parent
929
930      # Insert each '$text<wbr>$foo' before the old '$text$foo'
931      split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
932      for (split_string, last) in enumerate_with_last(split_by_wbr_list):
933          navigable_string.insert_before(split_string)
934
935          if not last:
936            # Note that 'insert' will move existing tags to this spot
937            # so make a new tag instead
938            navigable_string.insert_before(wbr_tag())
939
940      # Remove the old unmodified text
941      navigable_string.extract()
942
943  return soup.decode()
944