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