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