2 # Copyright (C) 2012 The Android Open Source Project
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
18 A set of helpers for rendering Mako templates with a Metadata model.
27 # Monkey-patch BS4. WBR element must not have an end tag.
28 bs4.builder.HTMLTreeBuilder.empty_element_tags.add("wbr")
30 from collections import OrderedDict
32 # Relative path from HTML file to the base directory used by <img> tags
33 IMAGE_SRC_METADATA="images/camera2/metadata/"
35 # Prepend this path to each <img src="foo"> in javadocs
36 JAVADOC_IMAGE_SRC_METADATA="../../../../" + IMAGE_SRC_METADATA
37 NDKDOC_IMAGE_SRC_METADATA="../" + IMAGE_SRC_METADATA
41 def _is_sec_or_ins(x):
42 return isinstance(x, metadata_model.Section) or \
43 isinstance(x, metadata_model.InnerNamespace)
49 def find_all_sections(root):
51 Find all descendants that are Section or InnerNamespace instances.
54 root: a Metadata instance
57 A list of Section/InnerNamespace instances
60 These are known as "sections" in the generated C code.
62 return root.find_all(_is_sec_or_ins)
64 def find_parent_section(entry):
66 Find the closest ancestor that is either a Section or InnerNamespace.
69 entry: an Entry or Clone node
72 An instance of Section or InnerNamespace
74 return entry.find_parent_first(_is_sec_or_ins)
76 # find uniquely named entries (w/o recursing through inner namespaces)
77 def find_unique_entries(node):
79 Find all uniquely named entries, without recursing through inner namespaces.
82 node: a Section or InnerNamespace instance
85 A sequence of MergedEntry nodes representing an entry
88 This collapses multiple entries with the same fully qualified name into
89 one entry (e.g. if there are multiple entries in different kinds).
91 if not isinstance(node, metadata_model.Section) and \
92 not isinstance(node, metadata_model.InnerNamespace):
93 raise TypeError("expected node to be a Section or InnerNamespace")
96 # remove the 'kinds' from the path between sec and the closest entries
97 # then search the immediate children of the search path
98 search_path = isinstance(node, metadata_model.Section) and node.kinds \
100 for i in search_path:
101 for entry in i.entries:
102 d[entry.name] = entry
104 for k,v in d.iteritems():
109 Calculate a period-separated string path from the root to this element,
110 by joining the names of each node and excluding the Metadata/Kind nodes
114 node: a Node instance
120 isa = lambda x,y: isinstance(x, y)
121 fltr = lambda x: not isa(x, metadata_model.Metadata) and \
122 not isa(x, metadata_model.Kind)
124 path = node.find_parents(fltr)
129 return ".".join((i.name for i in path))
133 Return the NDK version of given name, which replace
134 the leading "android" to "acamera"
137 name: name string of an entry
140 A NDK version name string of the input name
142 name_list = name.split(".")
143 if name_list[0] == "android":
144 name_list[0] = "acamera"
145 return ".".join(name_list)
147 def protobuf_type(entry):
149 Return the protocol buffer message type for input metadata entry.
150 Only support types used by static metadata right now
153 A string of protocol buffer type. Ex: "optional int32" or "repeated RangeInt"
156 if entry.typedef is None:
157 typeName = entry.type
159 typeName = entry.typedef.name
161 typename_to_protobuftype = {
162 "rational" : "Rational",
165 "rectangle" : "Rect",
166 "streamConfigurationMap" : "StreamConfigurations",
167 "rangeInt" : "RangeInt",
168 "rangeLong" : "RangeLong",
169 "colorSpaceTransform" : "ColorSpaceTransform",
170 "blackLevelPattern" : "BlackLevelPattern",
171 "byte" : "int32", # protocol buffer don't support byte
180 if typeName not in typename_to_protobuftype:
181 print >> sys.stderr,\
182 " ERROR: Could not find protocol buffer type for {%s} type {%s} typedef {%s}" % \
183 (entry.name, entry.type, entry.typedef)
185 proto_type = typename_to_protobuftype[typeName]
188 if entry.container == 'array':
189 has_variable_size = False
190 for size in entry.container_sizes:
194 has_variable_size = True
196 if has_variable_size:
199 return "%s %s" %(prefix, proto_type)
202 def protobuf_name(entry):
204 Return the protocol buffer field name for input metadata entry
207 A string. Ex: "android_colorCorrection_availableAberrationModes"
209 return entry.name.replace(".", "_")
211 def has_descendants_with_enums(node):
213 Determine whether or not the current node is or has any descendants with an
217 node: a Node instance
220 True if it finds an Enum node in the subtree, False otherwise
222 return bool(node.find_first(lambda x: isinstance(x, metadata_model.Enum)))
224 def get_children_by_throwing_away_kind(node, member='entries'):
226 Get the children of this node by compressing the subtree together by removing
227 the kind and then combining any children nodes with the same name together.
230 node: An instance of Section, InnerNamespace, or Kind
233 An iterable over the combined children of the subtree of node,
234 as if the Kinds never existed.
237 Not recursive. Call this function repeatedly on each child.
240 if isinstance(node, metadata_model.Section):
241 # Note that this makes jump from Section to Kind,
242 # skipping the Kind entirely in the tree.
243 node_to_combine = node.combine_kinds_into_single_node()
245 node_to_combine = node
247 combined_kind = node_to_combine.combine_children_by_name()
249 return (i for i in getattr(combined_kind, member))
251 def get_children_by_filtering_kind(section, kind_name, member='entries'):
253 Takes a section and yields the children of the merged kind under this section.
256 section: An instance of Section
257 kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
260 An iterable over the children of the specified merged kind.
263 matched_kind = next((i for i in section.merged_kinds if i.name == kind_name), None)
266 return getattr(matched_kind, member)
274 # abcDef.xyz -> ABC_DEF_XYZ
277 Convert an entry name string into an uppercase C symbol.
283 csym('abcDef.xyz') == 'ABC_DEF_XYZ'
286 newstr = "".join([i.isupper() and ("_" + i) or i for i in newstr]).upper()
287 newstr = newstr.replace(".", "_")
290 # abcDef.xyz -> abc_def_xyz
293 Convert an entry name string into a lowercase C symbol.
299 csyml('abcDef.xyz') == 'abc_def_xyz'
301 return csym(name).lower()
303 # pad with spaces to make string len == size. add new line if too big
304 def ljust(size, indent=4):
306 Creates a function that given a string will pad it with spaces to make
307 the string length == size. Adds a new line if the string was too big.
310 size: an integer representing how much spacing should be added
311 indent: an integer representing the initial indendation level
314 A function that takes a string and returns a string.
317 ljust(8)("hello") == 'hello '
320 Deprecated. Use pad instead since it works for non-first items in a
324 newstr = what.ljust(size)
325 if len(newstr) > size:
326 return what + "\n" + "".ljust(indent + size)
331 def _find_new_line():
333 if _context_buf is None:
334 raise ValueError("Context buffer was not set")
337 x = -1 # since the first read is always ''
339 while buf.tell() > 0 and buf.read(1) != '\n':
340 buf.seek(cur_pos - x)
347 # Pad the string until the buffer reaches the desired column.
348 # If string is too long, insert a new line with 'col' spaces instead
351 Create a function that given a string will pad it to the specified column col.
352 If the string overflows the column, put the string on a new line and pad it.
355 col: an integer specifying the column number
358 A function that given a string will produce a padded string.
361 pad(8)("hello") == 'hello '
364 This keeps track of the line written by Mako so far, so it will always
365 align to the column number correctly.
369 current_col = _find_new_line()
371 if len(what) > wut - current_col:
372 return what + "\n".ljust(col)
374 return what.ljust(wut - current_col)
377 # int32 -> TYPE_INT32, byte -> TYPE_BYTE, etc. note that enum -> TYPE_INT32
378 def ctype_enum(what):
380 Generate a camera_metadata_type_t symbol from a type string.
386 A string representing the camera_metadata_type_t
389 ctype_enum('int32') == 'TYPE_INT32'
390 ctype_enum('int64') == 'TYPE_INT64'
391 ctype_enum('float') == 'TYPE_FLOAT'
394 An enum is coerced to a byte since the rest of the camera_metadata
395 code doesn't support enums directly yet.
397 return 'TYPE_%s' %(what.upper())
400 # Calculate a java type name from an entry with a Typedef node
401 def _jtypedef_type(entry):
402 typedef = entry.typedef
405 # Hacky way to deal with arrays. Assume that if we have
406 # size 'Constant x N' the Constant is part of the Typedef size.
407 # So something sized just 'Constant', 'Constant1 x Constant2', etc
408 # is not treated as a real java array.
409 if entry.container == 'array':
410 has_variable_size = False
411 for size in entry.container_sizes:
415 has_variable_size = True
417 if has_variable_size:
421 name = typedef.languages['java']
423 return "%s%s" %(name, additional)
427 # Box if primitive. Otherwise leave unboxed.
428 def _jtype_box(type_name):
430 'boolean': 'Boolean',
438 return mapping.get(type_name, type_name)
440 def jtype_unboxed(entry):
442 Calculate the Java type from an entry type string, to be used whenever we
443 need the regular type in Java. It's not boxed, so it can't be used as a
444 generic type argument when the entry type happens to resolve to a primitive.
447 Since Java generics cannot be instantiated with primitives, this version
448 is not applicable in that case. Use jtype_boxed instead for that.
451 The string representing the Java type.
453 if not isinstance(entry, metadata_model.Entry):
454 raise ValueError("Expected entry to be an instance of Entry")
456 metadata_type = entry.type
461 typedef_name = _jtypedef_type(entry)
463 java_type = typedef_name # already takes into account arrays
466 if not java_type and entry.enum and metadata_type == 'byte':
467 # Always map byte enums to Java ints, unless there's a typedef override
477 'rational': 'Rational'
480 base_type = mapping[metadata_type]
482 # Convert to array (enums, basic types)
483 if entry.container == 'array':
488 java_type = '%s%s' %(base_type, additional)
490 # Now box this sucker.
493 def jtype_boxed(entry):
495 Calculate the Java type from an entry type string, to be used as a generic
496 type argument in Java. The type is guaranteed to inherit from Object.
498 It will only box when absolutely necessary, i.e. int -> Integer[], but
502 Since Java generics cannot be instantiated with primitives, this version
503 will use boxed types when absolutely required.
506 The string representing the boxed Java type.
508 unboxed_type = jtype_unboxed(entry)
509 return _jtype_box(unboxed_type)
511 def _is_jtype_generic(entry):
513 Determine whether or not the Java type represented by the entry type
514 string and/or typedef is a Java generic.
516 For example, "Range<Integer>" would be considered a generic, whereas
517 a "MeteringRectangle" or a plain "Integer" would not be considered a generic.
520 entry: An instance of an Entry node
523 True if it's a java generic, False otherwise.
526 local_typedef = _jtypedef_type(entry)
528 match = re.search(r'<.*>', local_typedef)
532 def _jtype_primitive(what):
534 Calculate the Java type from an entry type string.
537 Makes a special exception for Rational, since it's a primitive in terms of
538 the C-library camera_metadata type system.
541 The string representing the primitive type
549 'rational': 'Rational'
554 except KeyError as e:
555 raise ValueError("Can't map '%s' to a primitive, not supported" %what)
559 Calculate the java Class reference string for an entry.
565 <entry name="some_int" type="int32"/>
566 <entry name="some_int_array" type="int32" container='array'/>
568 jclass(some_int) == 'int.class'
569 jclass(some_int_array) == 'int[].class'
572 The ClassName.class string
575 return "%s.class" %jtype_unboxed(entry)
577 def jkey_type_token(entry):
579 Calculate the java type token compatible with a Key constructor.
580 This will be the Java Class<T> for non-generic classes, and a
581 TypeReference<T> for generic classes.
587 The ClassName.class string, or 'new TypeReference<ClassName>() {{ }}' string
589 if _is_jtype_generic(entry):
590 return "new TypeReference<%s>() {{ }}" %(jtype_boxed(entry))
594 def jidentifier(what):
596 Convert the input string into a valid Java identifier.
599 what: any identifier string
602 String with added underscores if necessary.
604 if re.match("\d", what):
609 def enum_calculate_value_string(enum_value):
611 Calculate the value of the enum, even if it does not have one explicitly
614 This looks back for the first enum value that has a predefined value and then
615 applies addition until we get the right value, using C-enum semantics.
618 enum_value: an EnumValue node with a valid Enum parent
623 <value id="5">Y</value>
627 enum_calculate_value_string(X) == '0'
628 enum_calculate_Value_string(Y) == '5'
629 enum_calculate_value_string(Z) == '6'
632 String that represents the enum value as an integer literal.
635 enum_value_siblings = list(enum_value.parent.values)
636 this_index = enum_value_siblings.index(enum_value)
638 def is_hex_string(instr):
639 return bool(re.match('0x[a-f0-9]+$', instr, re.IGNORECASE))
645 this_id = enum_value_siblings[this_index].id
646 while this_index != 0 and not this_id:
649 this_id = enum_value_siblings[this_index].id
652 base_value = int(this_id, 0) # guess base
653 emit_as_hex = is_hex_string(this_id)
656 return "0x%X" %(base_value + base_offset)
658 return "%d" %(base_value + base_offset)
660 def enumerate_with_last(iterable):
662 Enumerate a sequence of iterable, while knowing if this element is the last in
666 iterable: an Iterable of some sequence
669 (element, bool) where the bool is True iff the element is last in the seq.
671 it = (i for i in iterable)
673 first = next(it) # OK: raises exception if it is empty
675 second = first # for when we have only 1 element in iterable
680 # more elements remaining.
683 except StopIteration:
684 # last element. no more elements left
687 def pascal_case(what):
689 Convert the first letter of a string to uppercase, to make the identifier
690 conform to PascalCase.
692 If there are dots, remove the dots, and capitalize the letter following
693 where the dot was. Letters that weren't following dots are left unchanged,
694 except for the first letter of the string (which is made upper-case).
697 what: a string representing some identifier
700 String with first letter capitalized
703 pascal_case("helloWorld") == "HelloWorld"
704 pascal_case("foo") == "Foo"
705 pascal_case("hello.world") = "HelloWorld"
706 pascal_case("fooBar.fooBar") = "FooBarFooBar"
708 return "".join([s[0:1].upper() + s[1:] for s in what.split('.')])
710 def jkey_identifier(what):
712 Return a Java identifier from a property name.
715 what: a string representing a property name.
718 Java identifier corresponding to the property name. May need to be
719 prepended with the appropriate Java class name by the caller of this
720 function. Note that the outer namespace is stripped from the property
724 jkey_identifier("android.lens.facing") == "LENS_FACING"
726 return csym(what[what.find('.') + 1:])
728 def jenum_value(enum_entry, enum_value):
730 Calculate the Java name for an integer enum value
733 enum: An enum-typed Entry node
734 value: An EnumValue node for the enum
737 String representing the Java symbol
740 cname = csym(enum_entry.name)
741 return cname[cname.find('_') + 1:] + '_' + enum_value.name
743 def generate_extra_javadoc_detail(entry):
745 Returns a function to add extra details for an entry into a string for inclusion into
746 javadoc. Adds information about units, the list of enum values for this key, and the valid
751 text += '\n\n<b>Units</b>: %s\n' % (dedent(entry.units))
752 if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')):
753 text += '\n\n<b>Possible values:</b>\n<ul>\n'
754 for value in entry.enum.values:
756 text += ' <li>{@link #%s %s}</li>\n' % ( jenum_value(entry, value ), value.name )
759 if entry.enum and not (entry.typedef and entry.typedef.languages.get('java')):
760 text += '\n\n<b>Available values for this device:</b><br>\n'
762 text += '\n\n<b>Range of valid values:</b><br>\n'
763 text += '%s\n' % (dedent(entry.range))
764 if entry.hwlevel != 'legacy': # covers any of (None, 'limited', 'full')
765 text += '\n\n<b>Optional</b> - This value may be {@code null} on some devices.\n'
766 if entry.hwlevel == 'full':
768 '\n<b>Full capability</b> - \n' + \
769 'Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the\n' + \
770 'android.info.supportedHardwareLevel key\n'
771 if entry.hwlevel == 'limited':
773 '\n<b>Limited capability</b> - \n' + \
774 'Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the\n' + \
775 'android.info.supportedHardwareLevel key\n'
776 if entry.hwlevel == 'legacy':
777 text += "\nThis key is available on all devices."
783 def javadoc(metadata, indent = 4):
785 Returns a function to format a markdown syntax text block as a
786 javadoc comment section, given a set of metadata
789 metadata: A Metadata instance, representing the the top-level root
790 of the metadata for cross-referencing
791 indent: baseline level of indentation for javadoc block
793 A function that transforms a String text block as follows:
794 - Indent and * for insertion into a Javadoc comment block
795 - Trailing whitespace removed
796 - Entire body rendered via markdown to generate HTML
797 - All tag names converted to appropriate Javadoc {@link} with @see
801 "This is a comment for Javadoc\n" +
802 " with multiple lines, that should be \n" +
803 " formatted better\n" +
805 " That covers multiple lines as well\n"
806 " And references android.control.mode\n"
809 " * <p>This is a comment for Javadoc\n" +
810 " * with multiple lines, that should be\n" +
811 " * formatted better</p>\n" +
812 " * <p>That covers multiple lines as well</p>\n" +
813 " * and references {@link CaptureRequest#CONTROL_MODE android.control.mode}\n" +
815 " * @see CaptureRequest#CONTROL_MODE\n"
817 def javadoc_formatter(text):
818 comment_prefix = " " * indent + " * ";
820 # render with markdown => HTML
821 javatext = md(text, JAVADOC_IMAGE_SRC_METADATA)
823 # Identity transform for javadoc links
824 def javadoc_link_filter(target, shortname):
825 return '{@link %s %s}' % (target, shortname)
827 javatext = filter_links(javatext, javadoc_link_filter)
831 'static': 'CameraCharacteristics',
832 'dynamic': 'CaptureResult',
833 'controls': 'CaptureRequest' }
835 # Convert metadata entry "android.x.y.z" to form
836 # "{@link CaptureRequest#X_Y_Z android.x.y.z}"
837 def javadoc_crossref_filter(node):
838 if node.applied_visibility in ('public', 'java_public'):
839 return '{@link %s#%s %s}' % (kind_mapping[node.kind],
840 jkey_identifier(node.name),
845 # For each public tag "android.x.y.z" referenced, add a
846 # "@see CaptureRequest#X_Y_Z"
847 def javadoc_crossref_see_filter(node_set):
848 node_set = (x for x in node_set if x.applied_visibility in ('public', 'java_public'))
851 for node in node_set:
852 text = text + '\n@see %s#%s' % (kind_mapping[node.kind],
853 jkey_identifier(node.name))
855 return text if text != '\n' else ''
857 javatext = filter_tags(javatext, metadata, javadoc_crossref_filter, javadoc_crossref_see_filter)
859 def line_filter(line):
861 # Add ' * ' to it for stylistic reasons
862 # Strip right side of trailing whitespace
863 return (comment_prefix + line).rstrip()
865 # Process each line with above filter
866 javatext = "\n".join(line_filter(i) for i in javatext.split("\n")) + "\n"
870 return javadoc_formatter
872 def ndkdoc(metadata, indent = 4):
874 Returns a function to format a markdown syntax text block as a
875 NDK camera API C/C++ comment section, given a set of metadata
878 metadata: A Metadata instance, representing the the top-level root
879 of the metadata for cross-referencing
880 indent: baseline level of indentation for comment block
882 A function that transforms a String text block as follows:
883 - Indent and * for insertion into a comment block
884 - Trailing whitespace removed
885 - Entire body rendered via markdown
886 - All tag names converted to appropriate NDK tag name for each tag
889 "This is a comment for NDK\n" +
890 " with multiple lines, that should be \n" +
891 " formatted better\n" +
893 " That covers multiple lines as well\n"
894 " And references android.control.mode\n"
897 " * This is a comment for NDK\n" +
898 " * with multiple lines, that should be\n" +
899 " * formatted better\n" +
900 " * That covers multiple lines as well\n" +
901 " * and references ACAMERA_CONTROL_MODE\n" +
903 " * @see ACAMERA_CONTROL_MODE\n"
905 def ndkdoc_formatter(text):
906 # render with markdown => HTML
907 ndktext = md(text, NDKDOC_IMAGE_SRC_METADATA, False)
909 # Convert metadata entry "android.x.y.z" to form
910 # NDK tag format of "ACAMERA_X_Y_Z"
911 def ndkdoc_crossref_filter(node):
912 if node.applied_ndk_visible == 'true':
913 return csym(ndk(node.name))
917 # For each public tag "android.x.y.z" referenced, add a
918 # "@see ACAMERA_X_Y_Z"
919 def ndkdoc_crossref_see_filter(node_set):
920 node_set = (x for x in node_set if x.applied_ndk_visible == 'true')
923 for node in node_set:
924 text = text + '\n@see %s' % (csym(ndk(node.name)))
926 return text if text != '\n' else ''
928 ndktext = filter_tags(ndktext, metadata, ndkdoc_crossref_filter, ndkdoc_crossref_see_filter)
930 ndktext = ndk_replace_tag_wildcards(ndktext, metadata)
932 comment_prefix = " " * indent + " * ";
934 def line_filter(line):
936 # Add ' * ' to it for stylistic reasons
937 # Strip right side of trailing whitespace
938 return (comment_prefix + line).rstrip()
940 # Process each line with above filter
941 ndktext = "\n".join(line_filter(i) for i in ndktext.split("\n")) + "\n"
945 return ndkdoc_formatter
949 Remove all common indentation from every line but the 0th.
950 This will avoid getting <code> blocks when rendering text via markdown.
951 Ignoring the 0th line will also allow the 0th line not to be aligned.
954 text: A string of text to dedent.
957 String dedented by above rules.
960 assertEquals("bar\nline1\nline2", dedent("bar\n line1\n line2"))
961 assertEquals("bar\nline1\nline2", dedent(" bar\n line1\n line2"))
962 assertEquals("bar\n line1\nline2", dedent(" bar\n line1\n line2"))
964 text = textwrap.dedent(text)
965 text_lines = text.split('\n')
966 text_not_first = "\n".join(text_lines[1:])
967 text_not_first = textwrap.dedent(text_not_first)
968 text = text_lines[0] + "\n" + text_not_first
972 def md(text, img_src_prefix="", table_ext=True):
974 Run text through markdown to produce HTML.
976 This also removes all common indentation from every line but the 0th.
977 This will avoid getting <code> blocks in markdown.
978 Ignoring the 0th line will also allow the 0th line not to be aligned.
981 text: A markdown-syntax using block of text to format.
982 img_src_prefix: An optional string to prepend to each <img src="target"/>
985 String rendered by markdown and other rules applied (see above).
987 For example, this avoids the following situation:
991 <!--- can't use dedent directly since 'foo' has no indent -->
998 <!-- if no dedent is done generated code looks like -->
1005 Instead we get the more natural expected result:
1007 <!-- Good Output -->
1015 # full list of extensions at http://pythonhosted.org/Markdown/extensions/
1016 md_extensions = ['tables'] if table_ext else []# make <table> with ASCII |_| tables
1017 # render with markdown
1018 text = markdown.markdown(text, md_extensions)
1020 # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo">
1021 text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text)
1024 def filter_tags(text, metadata, filter_function, summary_function = None):
1026 Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in
1027 the provided text, and pass them through filter_function and summary_function.
1029 Used to linkify entry names in HMTL, javadoc output.
1032 text: A string representing a block of text destined for output
1033 metadata: A Metadata instance, the root of the metadata properties tree
1034 filter_function: A Node->string function to apply to each node
1035 when found in text; the string returned replaces the tag name in text.
1036 summary_function: A Node list->string function that is provided the list of
1037 unique tag nodes found in text, and which must return a string that is
1038 then appended to the end of the text. The list is sorted alphabetically
1043 def name_match(name):
1044 return lambda node: node.name == name
1046 # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure
1047 # to grab .z and not just outer_namespace.x.y. (sloppy, but since we
1048 # check for validity, a few false positives don't hurt).
1049 # Try to ignore items of the form {@link <outer_namespace>...
1050 for outer_namespace in metadata.outer_namespaces:
1052 tag_match = r"(?<!\{@link\s)" + outer_namespace.name + \
1053 r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)"
1055 def filter_sub(match):
1056 whole_match = match.group(0)
1057 section1 = match.group(1)
1058 section2 = match.group(2)
1059 section3 = match.group(3)
1060 end_slash = match.group(4)
1062 # Don't linkify things ending in slash (urls, for example)
1068 # First try a two-level match
1069 candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2)
1070 got_two_level = False
1072 node = metadata.find_first(name_match(candidate2.replace('\n','')))
1073 if not node and '\n' in section2:
1074 # Linefeeds are ambiguous - was the intent to add a space,
1075 # or continue a lengthy name? Try the former now.
1076 candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')])
1077 node = metadata.find_first(name_match(candidate2b))
1079 candidate2 = candidate2b
1082 # Have two-level match
1083 got_two_level = True
1084 candidate = candidate2
1086 # Try three-level match
1087 candidate3 = "%s%s" % (candidate2, section3)
1088 node = metadata.find_first(name_match(candidate3.replace('\n','')))
1090 if not node and '\n' in section3:
1091 # Linefeeds are ambiguous - was the intent to add a space,
1092 # or continue a lengthy name? Try the former now.
1093 candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')])
1094 node = metadata.find_first(name_match(candidate3b))
1096 candidate3 = candidate3b
1099 # Have 3-level match
1100 candidate = candidate3
1102 # Replace match with crossref or complain if a likely match couldn't be matched
1106 return whole_match.replace(candidate,filter_function(node))
1108 print >> sys.stderr,\
1109 " WARNING: Could not crossref likely reference {%s}" % (match.group(0))
1112 text = re.sub(tag_match, filter_sub, text)
1114 if summary_function is not None:
1115 return text + summary_function(sorted(tag_set, key=lambda x: x.name))
1119 def ndk_replace_tag_wildcards(text, metadata):
1121 Find all references to tags in the form android.xxx.* or android.xxx.yyy.*
1122 in the provided text, and replace them by NDK format of "ACAMERA_XXX_*" or
1126 text: A string representing a block of text destined for output
1127 metadata: A Metadata instance, the root of the metadata properties tree
1129 tag_match = r"android\.([a-zA-Z0-9\n]+)\.\*"
1130 tag_match_2 = r"android\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)\*"
1132 def filter_sub(match):
1133 return "ACAMERA_" + match.group(1).upper() + "_*"
1134 def filter_sub_2(match):
1135 return "ACAMERA_" + match.group(1).upper() + match.group(2).upper() + "_*"
1137 text = re.sub(tag_match, filter_sub, text)
1138 text = re.sub(tag_match_2, filter_sub_2, text)
1141 def filter_links(text, filter_function, summary_function = None):
1143 Find all references to tags in the form {@link xxx#yyy [zzz]} in the
1144 provided text, and pass them through filter_function and
1147 Used to linkify documentation cross-references in HMTL, javadoc output.
1150 text: A string representing a block of text destined for output
1151 metadata: A Metadata instance, the root of the metadata properties tree
1152 filter_function: A (string, string)->string function to apply to each 'xxx#yyy',
1153 zzz pair when found in text; the string returned replaces the tag name in text.
1154 summary_function: A string list->string function that is provided the list of
1155 unique targets found in text, and which must return a string that is
1156 then appended to the end of the text. The list is sorted alphabetically
1162 def name_match(name):
1163 return lambda node: node.name == name
1165 tag_match = r"\{@link\s+([^\s\}]+)([^\}]*)\}"
1167 def filter_sub(match):
1168 whole_match = match.group(0)
1169 target = match.group(1)
1170 shortname = match.group(2).strip()
1172 #print "Found link '%s' as '%s' -> '%s'" % (target, shortname, filter_function(target, shortname))
1174 # Replace match with crossref
1175 target_set.add(target)
1176 return filter_function(target, shortname)
1178 text = re.sub(tag_match, filter_sub, text)
1180 if summary_function is not None:
1181 return text + summary_function(sorted(target_set))
1185 def any_visible(section, kind_name, visibilities):
1187 Determine if entries in this section have an applied visibility that's in
1188 the list of given visibilities.
1191 section: A section of metadata
1192 kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls'
1193 visibilities: An iterable of visibilities to match against
1196 True if the section has any entries with any of the given visibilities. False otherwise.
1199 for inner_namespace in get_children_by_filtering_kind(section, kind_name,
1201 if any(filter_visibility(inner_namespace.merged_entries, visibilities)):
1204 return any(filter_visibility(get_children_by_filtering_kind(section, kind_name,
1209 def filter_visibility(entries, visibilities):
1211 Remove entries whose applied visibility is not in the supplied visibilities.
1214 entries: An iterable of Entry nodes
1215 visibilities: An iterable of visibilities to filter against
1218 An iterable of Entry nodes
1220 return (e for e in entries if e.applied_visibility in visibilities)
1222 def remove_synthetic(entries):
1224 Filter the given entries by removing those that are synthetic.
1227 entries: An iterable of Entry nodes
1230 An iterable of Entry nodes
1232 return (e for e in entries if not e.synthetic)
1234 def filter_ndk_visible(entries):
1236 Filter the given entries by removing those that are not NDK visible.
1239 entries: An iterable of Entry nodes
1242 An iterable of Entry nodes
1244 return (e for e in entries if e.applied_ndk_visible == 'true')
1248 Insert word break hints for the browser in the form of <wbr> HTML tags.
1250 Word breaks are inserted inside an HTML node only, so the nodes themselves
1251 will not be changed. Attributes are also left unchanged.
1253 The following rules apply to insert word breaks:
1254 - For characters in [ '.', '/', '_' ]
1255 - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts)
1258 text: A string of text containing HTML content.
1261 A string with <wbr> inserted by the above rules.
1263 SPLIT_CHARS_LIST = ['.', '_', '/']
1264 SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters
1265 CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z
1266 def wbr_filter(text):
1269 # for johnyOrange.appleCider.redGuardian also insert wbr before the caps
1270 # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian
1271 for words in text.split(" "):
1272 for char in SPLIT_CHARS_LIST:
1273 # match at least x.y.z, don't match x or x.y
1274 if len(words.split(char)) >= CAP_LETTER_MIN:
1275 new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words)
1276 new_txt = new_txt.replace(words, new_word)
1278 # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z.
1279 new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt)
1283 # Do not mangle HTML when doing the replace by using BeatifulSoup
1284 # - Use the 'html.parser' to avoid inserting <html><body> when decoding
1285 soup = bs4.BeautifulSoup(text, features='html.parser')
1286 wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time
1288 for navigable_string in soup.findAll(text=True):
1289 parent = navigable_string.parent
1291 # Insert each '$text<wbr>$foo' before the old '$text$foo'
1292 split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>")
1293 for (split_string, last) in enumerate_with_last(split_by_wbr_list):
1294 navigable_string.insert_before(split_string)
1297 # Note that 'insert' will move existing tags to this spot
1298 # so make a new tag instead
1299 navigable_string.insert_before(wbr_tag())
1301 # Remove the old unmodified text
1302 navigable_string.extract()
1304 return soup.decode()