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