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 """ 18 A set of helpers for rendering Mako templates with a Metadata model. 19 """ 20 21 import metadata_model 22 import re 23 import markdown 24 import textwrap 25 import sys 26 import bs4 27 # Monkey-patch BS4. WBR element must not have an end tag. 28 bs4.builder.HTMLTreeBuilder.empty_element_tags.add("wbr") 29 30 from collections import OrderedDict 31 32 # Relative path from HTML file to the base directory used by <img> tags 33 IMAGE_SRC_METADATA="images/camera2/metadata/" 34 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 38 39 _context_buf = None 40 41 def _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 49 def 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 64 def 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) 77 def 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 107 def 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 131 def 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 147 def 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 202 def 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 211 def 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 224 def 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 251 def 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 275 def 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 291 def 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 304 def 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 331 def _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 349 def 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 378 def 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 401 def _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. 428 def _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 440 def 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 493 def 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 511 def _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 532 def _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 557 def 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 577 def 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 594 def 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 609 def 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 660 def 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 687 def 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 710 def 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 728 def 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 743 def 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 783 def 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 872 def 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 # Turn off the table plugin since doxygen doesn't recognize generated <thead> <tbody> tags 908 ndktext = md(text, NDKDOC_IMAGE_SRC_METADATA, False) 909 910 # Convert metadata entry "android.x.y.z" to form 911 # NDK tag format of "ACAMERA_X_Y_Z" 912 def ndkdoc_crossref_filter(node): 913 if node.applied_ndk_visible == 'true': 914 return csym(ndk(node.name)) 915 else: 916 return node.name 917 918 # For each public tag "android.x.y.z" referenced, add a 919 # "@see ACAMERA_X_Y_Z" 920 def ndkdoc_crossref_see_filter(node_set): 921 node_set = (x for x in node_set if x.applied_ndk_visible == 'true') 922 923 text = '\n' 924 for node in node_set: 925 text = text + '\n@see %s' % (csym(ndk(node.name))) 926 927 return text if text != '\n' else '' 928 929 ndktext = filter_tags(ndktext, metadata, ndkdoc_crossref_filter, ndkdoc_crossref_see_filter) 930 931 ndktext = ndk_replace_tag_wildcards(ndktext, metadata) 932 933 comment_prefix = " " * indent + " * "; 934 935 def line_filter(line): 936 # Indent each line 937 # Add ' * ' to it for stylistic reasons 938 # Strip right side of trailing whitespace 939 return (comment_prefix + line).rstrip() 940 941 # Process each line with above filter 942 ndktext = "\n".join(line_filter(i) for i in ndktext.split("\n")) + "\n" 943 944 return ndktext 945 946 return ndkdoc_formatter 947 948 def dedent(text): 949 """ 950 Remove all common indentation from every line but the 0th. 951 This will avoid getting <code> blocks when rendering text via markdown. 952 Ignoring the 0th line will also allow the 0th line not to be aligned. 953 954 Args: 955 text: A string of text to dedent. 956 957 Returns: 958 String dedented by above rules. 959 960 For example: 961 assertEquals("bar\nline1\nline2", dedent("bar\n line1\n line2")) 962 assertEquals("bar\nline1\nline2", dedent(" bar\n line1\n line2")) 963 assertEquals("bar\n line1\nline2", dedent(" bar\n line1\n line2")) 964 """ 965 text = textwrap.dedent(text) 966 text_lines = text.split('\n') 967 text_not_first = "\n".join(text_lines[1:]) 968 text_not_first = textwrap.dedent(text_not_first) 969 text = text_lines[0] + "\n" + text_not_first 970 971 return text 972 973 def md(text, img_src_prefix="", table_ext=True): 974 """ 975 Run text through markdown to produce HTML. 976 977 This also removes all common indentation from every line but the 0th. 978 This will avoid getting <code> blocks in markdown. 979 Ignoring the 0th line will also allow the 0th line not to be aligned. 980 981 Args: 982 text: A markdown-syntax using block of text to format. 983 img_src_prefix: An optional string to prepend to each <img src="target"/> 984 985 Returns: 986 String rendered by markdown and other rules applied (see above). 987 988 For example, this avoids the following situation: 989 990 <!-- Input --> 991 992 <!--- can't use dedent directly since 'foo' has no indent --> 993 <notes>foo 994 bar 995 bar 996 </notes> 997 998 <!-- Bad Output -- > 999 <!-- if no dedent is done generated code looks like --> 1000 <p>foo 1001 <code><pre> 1002 bar 1003 bar</pre></code> 1004 </p> 1005 1006 Instead we get the more natural expected result: 1007 1008 <!-- Good Output --> 1009 <p>foo 1010 bar 1011 bar</p> 1012 1013 """ 1014 text = dedent(text) 1015 1016 # full list of extensions at http://pythonhosted.org/Markdown/extensions/ 1017 md_extensions = ['tables'] if table_ext else []# make <table> with ASCII |_| tables 1018 # render with markdown 1019 text = markdown.markdown(text, md_extensions) 1020 1021 # prepend a prefix to each <img src="foo"> -> <img src="${prefix}foo"> 1022 text = re.sub(r'src="([^"]*)"', 'src="' + img_src_prefix + r'\1"', text) 1023 return text 1024 1025 def filter_tags(text, metadata, filter_function, summary_function = None): 1026 """ 1027 Find all references to tags in the form outer_namespace.xxx.yyy[.zzz] in 1028 the provided text, and pass them through filter_function and summary_function. 1029 1030 Used to linkify entry names in HMTL, javadoc output. 1031 1032 Args: 1033 text: A string representing a block of text destined for output 1034 metadata: A Metadata instance, the root of the metadata properties tree 1035 filter_function: A Node->string function to apply to each node 1036 when found in text; the string returned replaces the tag name in text. 1037 summary_function: A Node list->string function that is provided the list of 1038 unique tag nodes found in text, and which must return a string that is 1039 then appended to the end of the text. The list is sorted alphabetically 1040 by node name. 1041 """ 1042 1043 tag_set = set() 1044 def name_match(name): 1045 return lambda node: node.name == name 1046 1047 # Match outer_namespace.x.y or outer_namespace.x.y.z, making sure 1048 # to grab .z and not just outer_namespace.x.y. (sloppy, but since we 1049 # check for validity, a few false positives don't hurt). 1050 # Try to ignore items of the form {@link <outer_namespace>... 1051 for outer_namespace in metadata.outer_namespaces: 1052 1053 tag_match = r"(?<!\{@link\s)" + outer_namespace.name + \ 1054 r"\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)(\.[a-zA-Z0-9\n]+)?([/]?)" 1055 1056 def filter_sub(match): 1057 whole_match = match.group(0) 1058 section1 = match.group(1) 1059 section2 = match.group(2) 1060 section3 = match.group(3) 1061 end_slash = match.group(4) 1062 1063 # Don't linkify things ending in slash (urls, for example) 1064 if end_slash: 1065 return whole_match 1066 1067 candidate = "" 1068 1069 # First try a two-level match 1070 candidate2 = "%s.%s.%s" % (outer_namespace.name, section1, section2) 1071 got_two_level = False 1072 1073 node = metadata.find_first(name_match(candidate2.replace('\n',''))) 1074 if not node and '\n' in section2: 1075 # Linefeeds are ambiguous - was the intent to add a space, 1076 # or continue a lengthy name? Try the former now. 1077 candidate2b = "%s.%s.%s" % (outer_namespace.name, section1, section2[:section2.find('\n')]) 1078 node = metadata.find_first(name_match(candidate2b)) 1079 if node: 1080 candidate2 = candidate2b 1081 1082 if node: 1083 # Have two-level match 1084 got_two_level = True 1085 candidate = candidate2 1086 elif section3: 1087 # Try three-level match 1088 candidate3 = "%s%s" % (candidate2, section3) 1089 node = metadata.find_first(name_match(candidate3.replace('\n',''))) 1090 1091 if not node and '\n' in section3: 1092 # Linefeeds are ambiguous - was the intent to add a space, 1093 # or continue a lengthy name? Try the former now. 1094 candidate3b = "%s%s" % (candidate2, section3[:section3.find('\n')]) 1095 node = metadata.find_first(name_match(candidate3b)) 1096 if node: 1097 candidate3 = candidate3b 1098 1099 if node: 1100 # Have 3-level match 1101 candidate = candidate3 1102 1103 # Replace match with crossref or complain if a likely match couldn't be matched 1104 1105 if node: 1106 tag_set.add(node) 1107 return whole_match.replace(candidate,filter_function(node)) 1108 else: 1109 print >> sys.stderr,\ 1110 " WARNING: Could not crossref likely reference {%s}" % (match.group(0)) 1111 return whole_match 1112 1113 text = re.sub(tag_match, filter_sub, text) 1114 1115 if summary_function is not None: 1116 return text + summary_function(sorted(tag_set, key=lambda x: x.name)) 1117 else: 1118 return text 1119 1120 def ndk_replace_tag_wildcards(text, metadata): 1121 """ 1122 Find all references to tags in the form android.xxx.* or android.xxx.yyy.* 1123 in the provided text, and replace them by NDK format of "ACAMERA_XXX_*" or 1124 "ACAMERA_XXX_YYY_*" 1125 1126 Args: 1127 text: A string representing a block of text destined for output 1128 metadata: A Metadata instance, the root of the metadata properties tree 1129 """ 1130 tag_match = r"android\.([a-zA-Z0-9\n]+)\.\*" 1131 tag_match_2 = r"android\.([a-zA-Z0-9\n]+)\.([a-zA-Z0-9\n]+)\*" 1132 1133 def filter_sub(match): 1134 return "ACAMERA_" + match.group(1).upper() + "_*" 1135 def filter_sub_2(match): 1136 return "ACAMERA_" + match.group(1).upper() + match.group(2).upper() + "_*" 1137 1138 text = re.sub(tag_match, filter_sub, text) 1139 text = re.sub(tag_match_2, filter_sub_2, text) 1140 return text 1141 1142 def filter_links(text, filter_function, summary_function = None): 1143 """ 1144 Find all references to tags in the form {@link xxx#yyy [zzz]} in the 1145 provided text, and pass them through filter_function and 1146 summary_function. 1147 1148 Used to linkify documentation cross-references in HMTL, javadoc output. 1149 1150 Args: 1151 text: A string representing a block of text destined for output 1152 metadata: A Metadata instance, the root of the metadata properties tree 1153 filter_function: A (string, string)->string function to apply to each 'xxx#yyy', 1154 zzz pair when found in text; the string returned replaces the tag name in text. 1155 summary_function: A string list->string function that is provided the list of 1156 unique targets found in text, and which must return a string that is 1157 then appended to the end of the text. The list is sorted alphabetically 1158 by node name. 1159 1160 """ 1161 1162 target_set = set() 1163 def name_match(name): 1164 return lambda node: node.name == name 1165 1166 tag_match = r"\{@link\s+([^\s\}]+)([^\}]*)\}" 1167 1168 def filter_sub(match): 1169 whole_match = match.group(0) 1170 target = match.group(1) 1171 shortname = match.group(2).strip() 1172 1173 #print "Found link '%s' as '%s' -> '%s'" % (target, shortname, filter_function(target, shortname)) 1174 1175 # Replace match with crossref 1176 target_set.add(target) 1177 return filter_function(target, shortname) 1178 1179 text = re.sub(tag_match, filter_sub, text) 1180 1181 if summary_function is not None: 1182 return text + summary_function(sorted(target_set)) 1183 else: 1184 return text 1185 1186 def any_visible(section, kind_name, visibilities): 1187 """ 1188 Determine if entries in this section have an applied visibility that's in 1189 the list of given visibilities. 1190 1191 Args: 1192 section: A section of metadata 1193 kind_name: A name of the kind, i.e. 'dynamic' or 'static' or 'controls' 1194 visibilities: An iterable of visibilities to match against 1195 1196 Returns: 1197 True if the section has any entries with any of the given visibilities. False otherwise. 1198 """ 1199 1200 for inner_namespace in get_children_by_filtering_kind(section, kind_name, 1201 'namespaces'): 1202 if any(filter_visibility(inner_namespace.merged_entries, visibilities)): 1203 return True 1204 1205 return any(filter_visibility(get_children_by_filtering_kind(section, kind_name, 1206 'merged_entries'), 1207 visibilities)) 1208 1209 1210 def filter_visibility(entries, visibilities): 1211 """ 1212 Remove entries whose applied visibility is not in the supplied visibilities. 1213 1214 Args: 1215 entries: An iterable of Entry nodes 1216 visibilities: An iterable of visibilities to filter against 1217 1218 Yields: 1219 An iterable of Entry nodes 1220 """ 1221 return (e for e in entries if e.applied_visibility in visibilities) 1222 1223 def remove_synthetic(entries): 1224 """ 1225 Filter the given entries by removing those that are synthetic. 1226 1227 Args: 1228 entries: An iterable of Entry nodes 1229 1230 Yields: 1231 An iterable of Entry nodes 1232 """ 1233 return (e for e in entries if not e.synthetic) 1234 1235 def filter_ndk_visible(entries): 1236 """ 1237 Filter the given entries by removing those that are not NDK visible. 1238 1239 Args: 1240 entries: An iterable of Entry nodes 1241 1242 Yields: 1243 An iterable of Entry nodes 1244 """ 1245 return (e for e in entries if e.applied_ndk_visible == 'true') 1246 1247 def wbr(text): 1248 """ 1249 Insert word break hints for the browser in the form of <wbr> HTML tags. 1250 1251 Word breaks are inserted inside an HTML node only, so the nodes themselves 1252 will not be changed. Attributes are also left unchanged. 1253 1254 The following rules apply to insert word breaks: 1255 - For characters in [ '.', '/', '_' ] 1256 - For uppercase letters inside a multi-word X.Y.Z (at least 3 parts) 1257 1258 Args: 1259 text: A string of text containing HTML content. 1260 1261 Returns: 1262 A string with <wbr> inserted by the above rules. 1263 """ 1264 SPLIT_CHARS_LIST = ['.', '_', '/'] 1265 SPLIT_CHARS = r'([.|/|_/,]+)' # split by these characters 1266 CAP_LETTER_MIN = 3 # at least 3 components split by above chars, i.e. x.y.z 1267 def wbr_filter(text): 1268 new_txt = text 1269 1270 # for johnyOrange.appleCider.redGuardian also insert wbr before the caps 1271 # => johny<wbr>Orange.apple<wbr>Cider.red<wbr>Guardian 1272 for words in text.split(" "): 1273 for char in SPLIT_CHARS_LIST: 1274 # match at least x.y.z, don't match x or x.y 1275 if len(words.split(char)) >= CAP_LETTER_MIN: 1276 new_word = re.sub(r"([a-z])([A-Z])", r"\1<wbr>\2", words) 1277 new_txt = new_txt.replace(words, new_word) 1278 1279 # e.g. X/Y/Z -> X/<wbr>Y/<wbr>/Z. also for X.Y.Z, X_Y_Z. 1280 new_txt = re.sub(SPLIT_CHARS, r"\1<wbr>", new_txt) 1281 1282 return new_txt 1283 1284 # Do not mangle HTML when doing the replace by using BeatifulSoup 1285 # - Use the 'html.parser' to avoid inserting <html><body> when decoding 1286 soup = bs4.BeautifulSoup(text, features='html.parser') 1287 wbr_tag = lambda: soup.new_tag('wbr') # must generate new tag every time 1288 1289 for navigable_string in soup.findAll(text=True): 1290 parent = navigable_string.parent 1291 1292 # Insert each '$text<wbr>$foo' before the old '$text$foo' 1293 split_by_wbr_list = wbr_filter(navigable_string).split("<wbr>") 1294 for (split_string, last) in enumerate_with_last(split_by_wbr_list): 1295 navigable_string.insert_before(split_string) 1296 1297 if not last: 1298 # Note that 'insert' will move existing tags to this spot 1299 # so make a new tag instead 1300 navigable_string.insert_before(wbr_tag()) 1301 1302 # Remove the old unmodified text 1303 navigable_string.extract() 1304 1305 return soup.decode() 1306