refspy.manager
A facade for all major interface functions.
See refspy.refspy() for a useful helper function.
1"""A facade for all major interface functions. 2 3See `refspy.refspy()` for a useful helper function. 4""" 5 6import re 7from collections.abc import Generator 8from pydantic import TypeAdapter 9 10from refspy.models.book import Book 11from refspy.models.language import Language 12from refspy.models.library import Library 13from refspy.models.range import combine_ranges, merge_ranges, range 14from refspy.models.reference import ( 15 Reference, 16 book_reference, 17 chapter_reference, 18 join_references, 19 reference, 20 split_reference, 21 sort_references, 22 unique_references, 23 verse_reference, 24) 25from refspy.models.syntax import Syntax 26from refspy.models.verse import verse 27 28from refspy.types.number import Number 29 30from refspy.formatter import Formatter 31from refspy.indexers import ( 32 index_book_aliases, 33 index_books, 34 index_libraries, 35) 36from refspy.matcher import Matcher 37from refspy.navigator import Navigator 38from refspy.utils import url_param, url_escape 39 40""" 41References can always be formatted with Manager.template(ref). If no 42pattern argument is supplied, the default short format will be used, e.g. 43'Rom 12:1-7'. or ref.abbrev_name() 44""" 45 46 47class Manager: 48 """ 49 The manager object integrates the main library features into a single 50 convenient facade. 51 """ 52 53 def __init__( 54 self, 55 libraries: list[Library], 56 language: Language, 57 syntax: Syntax | None = None, 58 include_two_letter_aliases: bool = True, 59 ): 60 """ 61 Construct a new Manager object. 62 63 Args: 64 libraries: A list of libraries like NT, OT. 65 language: A language object like ENGLISH. 66 language: A syntax object like INTERNATIONAL. 67 include_two_letter_aliases: Whether to allow `len(alias) == 2` 68 (default: True) 69 """ 70 self.libraries: dict[Number, Library] = index_libraries(libraries) 71 """A lookup dictionary for Libraries by library.id """ 72 73 self.books: dict[tuple[Number, Number], Book] = index_books(libraries) 74 """A lookup dictionary for Books by (library.id, book.id)""" 75 76 self.book_aliases: dict[str, tuple[Number, Number]] = index_book_aliases( 77 libraries, include_two_letter_aliases=include_two_letter_aliases 78 ) 79 """A lookup dictionary for (library.id, book.id) by book alias strings.""" 80 81 self.language: Language = language 82 """Language-specific program data.""" 83 84 self.syntax: Syntax = syntax or language.syntax 85 """Syntax-specific program data.""" 86 87 self.matcher: Matcher = Matcher( 88 self.books, self.book_aliases, self.language, self.syntax 89 ) 90 """Delegate reference matching tasks.""" 91 92 self.formatter: Formatter = Formatter(self.books, self.book_aliases) 93 """Delegate formatting tasks.""" 94 95 self.navigator: Navigator = Navigator(self.books, self.book_aliases) 96 """Delegate navigation tasks.""" 97 98 # ----------------------------------- 99 # Index and summary functions 100 # ----------------------------------- 101 102 def make_index_references_by_chapter( 103 self, references: list[Reference] 104 ) -> list[Reference]: 105 """Return a sorted, combined, simplified list of references by book.""" 106 collation = self.collate_chapter_references( 107 sorted([ref for ref in references if ref and not ref.is_book()]) 108 ) 109 indexes = [] 110 for _, book_collation in collation: 111 for _, chapter_collation in book_collation: 112 for _, reference_list in chapter_collation: 113 compact_ref = join_references(reference_list) 114 indexes.append(compact_ref) 115 return indexes 116 117 def make_index( 118 self, 119 references: list[Reference], 120 pattern: str | None = None, 121 ) -> str | None: 122 """ 123 Return a one-line list of sorted references. No merging or combining. 124 125 Args: 126 pattern: a formatting string suitable for links, see 127 `refspy.manager.Manager.template()`. 128 129 Note: 130 If no template pattern is provided, reference formatting 131 will default to `refspy.manager.Manager.abbrev_name()` 132 """ 133 if indexes := self.make_index_references_by_chapter(references): 134 return self.template(join_references(indexes)) 135 else: 136 return None 137 138 def make_summary_references(self, references: list[Reference]) -> list[Reference]: 139 """Return a sorted, combined, simplified list of References.""" 140 collation = self.collate( 141 sorted([ref for ref in references if ref and not ref.is_book()]) 142 ) 143 summary = [] 144 for _, book_collation in collation: 145 for _, reference_list in book_collation: 146 compact_ref = self.combine_references(reference_list) 147 summary.append(compact_ref) 148 return summary 149 150 def make_summary( 151 self, references: list[Reference], pattern: str | None = None 152 ) -> str | None: 153 """ 154 Return a string showing a sorted, combined, list of References. 155 156 Args: 157 pattern: a formatting string suitable for links, see 158 `refspy.manager.Manager.template()`. 159 160 Note: 161 If no template pattern is provided, reference formatting 162 will default to `refspy.manager.Manager.abbrev_name()`. 163 164 Args: 165 max_chapters: The maximum number of chapter hotspots to return. 166 min_references: The minimal references per chapter that qualifies as a hotspot. 167 """ 168 if summaries := self.make_summary_references(references): 169 return "; ".join([self.template(ref, pattern) for ref in summaries]) 170 else: 171 return None 172 173 def make_summary_references_by_chapter( 174 self, references: list[Reference] 175 ) -> list[Reference]: 176 """Return a sorted, combined, simplified list of references by book.""" 177 collation = self.collate_chapter_references( 178 sorted([ref for ref in references if ref and not ref.is_book()]) 179 ) 180 summary = [] 181 for _, book_collation in collation: 182 for _, chapter_collation in book_collation: 183 for _, reference_list in chapter_collation: 184 compact_ref = self.combine_references(reference_list) 185 summary.append(compact_ref) 186 return summary 187 188 def make_summary_by_chapter( 189 self, references: list[Reference], pattern: str | None = None 190 ) -> str | None: 191 """ 192 Return a string showing a sorted, combined, list of References. 193 194 Args: 195 pattern: a formatting string suitable for links, see 196 `refspy.manager.Manager.template()`. 197 198 Note: 199 If no template pattern is provided, reference formatting 200 will default to `refspy.manager.Manager.abbrev_name()`. 201 202 Args: 203 a max_chapters: The maximum number of chapter hotspots to return. 204 min_references: The minimal references per chapter that qualifies as a hotspot. 205 """ 206 if summaries := self.make_summary_references_by_chapter(references): 207 return ", ".join([self.template(ref, pattern) for ref in summaries]) 208 else: 209 return None 210 211 def make_hotspot_tuples( 212 self, 213 references: list[Reference], 214 max_chapters: int = 7, 215 min_references: int = 2, 216 ) -> list[tuple[Reference, int]]: 217 """ 218 Find the most referenced chapters in a set of references. 219 220 Args: 221 max_chapters: The maximum number of chapter hotspots to return. 222 min_references: The minimal references per chapter that qualifies as a hotspot. 223 224 Return: 225 a list of tuples of (chapter reference, count) in descending frequency 226 """ 227 if not references: 228 return [] 229 totals = dict() 230 for ref in sort_references(references): 231 for _ in ref.ranges: 232 for tuple in [ 233 (_.start.library, _.start.book, _.start.chapter), 234 (_.end.library, _.end.book, _.end.chapter), 235 ]: 236 if tuple in totals: 237 totals[tuple] += 1 238 else: 239 totals[tuple] = 1 240 hotspots = [ 241 (chapter_reference(*tuple), int(total / 2)) 242 for tuple, total in totals.items() 243 if total / 2 >= min_references 244 ] 245 hotspots_desc = sorted(hotspots, key=lambda item: item[1], reverse=True) 246 return hotspots_desc[:max_chapters] 247 248 def make_hotspot_references( 249 self, 250 references: list[Reference], 251 max_chapters: int = 7, 252 min_references: int = 2, 253 ) -> list[Reference]: 254 """ 255 Return chapter references in descending order of frequency. 256 257 Args: 258 max_chapters: The maximum number of chapter hotspots to return. 259 min_references: The minimal references per chapter that qualifies as a hotspot. 260 """ 261 tuples = self.make_hotspot_tuples(references, max_chapters, min_references) 262 return [ref for ref, _ in tuples] 263 264 def make_hotspots( 265 self, 266 references: list[Reference], 267 max_chapters: int = 7, 268 min_references: int = 2, 269 pattern: str | None = None, 270 ) -> str | None: 271 """ 272 Return chapter references in descending order of frequency, as a text 273 string, e.g. "Rom 3, 1 Cor 6", or None. 274 275 Args: 276 max_chapters: The maximum number of chapter hotspots to return. 277 min_references: The minimal references per chapter that qualifies as a hotspot. 278 pattern: an optional template pattern for formatting references 279 """ 280 if refs := self.make_hotspot_references( 281 references, max_chapters=max_chapters, min_references=min_references 282 ): 283 return ", ".join([self.template(ref, pattern) for ref in refs]) 284 else: 285 return None 286 287 # ----------------------------------- 288 # Merging functions 289 # ----------------------------------- 290 291 def sort(self, references: list[Reference]) -> list[Reference]: 292 """Sort a list of references by comparing its lists of ranges""" 293 return sort_references(references) 294 295 def unique(self, references: list[Reference]) -> list[Reference]: 296 """Return unique entries in an already sorted list. 297 298 Note: 299 - Like `uniq` on UNIX. 300 """ 301 return unique_references(references) 302 303 def split(self, reference: Reference) -> list[Reference]: 304 """Split a reference into a list, with one range per reference 305 306 TODO: 307 - Add split by book, chapter? 308 """ 309 return split_reference(reference) 310 311 def join(self, references: list[Reference]) -> Reference: 312 """Join a list of references into a single reference 313 314 TODO: 315 - Add join by book, chapter? 316 """ 317 return join_references(references) 318 319 def sort_references(self, references: list[Reference]) -> Reference: 320 """ 321 DEPRECATED for unclear naming: will be removed before 1.0 322 323 Return a single reference containing sorted ranges from a list of 324 references. 325 326 Superceded by `join_references(sort_references(references))` or just 327 `__.join(__.sort(references))` in Manager. 328 """ 329 all_ranges = [_ for ref in references for _ in ref.ranges] 330 return Reference(ranges=sorted(all_ranges)) 331 332 def merge_references(self, references: list[Reference]) -> Reference: 333 """For a list of references, merge their ranges into a new reference. 334 335 Merging means combining ranges that overlap. 336 """ 337 ranges = [] 338 for ref in references: 339 ranges.extend(ref.ranges) 340 new_ref = reference(*merge_ranges(ranges)) 341 return new_ref 342 343 def combine_references(self, references: list[Reference]) -> Reference: 344 """For a list of references, combine their ranges into a new reference. 345 346 Combining means sorting ranges, merging overlaps, and joining adjacent 347 records. 348 """ 349 ranges = [] 350 for ref in references: 351 ranges.extend(ref.ranges) 352 new_ref = reference(*combine_ranges(ranges)) 353 return new_ref 354 355 # ----------------------------------- 356 # Iteration functions 357 # ----------------------------------- 358 359 def collate( 360 self, references: list[Reference] 361 ) -> list[tuple[Library, list[tuple[Book, list[Reference]]]]]: 362 """ 363 The default collation is by book; use collate_chapter_references 364 otherwise. 365 """ 366 return self.collate_book_references(references) 367 368 def collate_book_references( 369 self, references: list[Reference] 370 ) -> list[tuple[Library, list[tuple[Book, list[Reference]]]]]: 371 """ 372 Collate references into nested library and book dictionaries for easy 373 looping. 374 375 Example: 376 ``` 377 for library, book_collation in __.collate_books_references(refs): 378 for book, references in book_collation: 379 # ... 380 ``` 381 """ 382 library_list = list() 383 for library_id, books_dict in self.collate_by_book(references).items(): 384 book_list = list() 385 for book_id, references in books_dict.items(): 386 book_list.append(tuple([self.books[library_id, book_id], references])) 387 library_list.append(tuple([self.libraries[library_id], book_list])) 388 return library_list 389 390 def collate_by_book( 391 self, references: list[Reference] 392 ) -> dict[Number, dict[Number, list[Reference]]]: 393 """ 394 A collation groups single-book references by library and book IDs. 395 Multi-book references are ignored. 396 """ 397 collation = dict() 398 for ref in [_ for _ in references if _.count_books() == 1]: 399 v1 = ref.ranges[0].start 400 if v1.library not in collation: 401 collation[v1.library] = dict() 402 if v1.book not in collation[v1.library]: 403 collation[v1.library][v1.book] = list() 404 collation[v1.library][v1.book].append(ref) 405 return collation 406 407 def collate_chapter_references( 408 self, references: list[Reference] 409 ) -> list[tuple[Library, list[tuple[Book, list[tuple[Number, list[Reference]]]]]]]: 410 """ 411 Collate references into nested library, book, and chapter dictionaries 412 for easy looping. 413 414 Example: 415 ``` 416 for library, book_collation in __.collate_chapter_references(refs): 417 for book, chapter_collation in book_collation: 418 for chapter_number, references in chapter_collation: 419 # ... 420 ``` 421 """ 422 library_list = list() 423 for library_id, books_dict in self.collate_by_chapter(references).items(): 424 book_list = list() 425 for book_id, chapters_dict in books_dict.items(): 426 chapter_list = list() 427 for chapter_id, references in chapters_dict.items(): 428 chapter_list.append(tuple([chapter_id, references])) 429 book_list.append(tuple([self.books[library_id, book_id], chapter_list])) 430 library_list.append(tuple([self.libraries[library_id], book_list])) 431 return library_list 432 433 def collate_by_chapter( 434 self, references: list[Reference] 435 ) -> dict[Number, dict[Number, dict[Number, list[Reference]]]]: 436 """ 437 A collation groups single-book references by library, book, and chapter 438 IDs. Multi-book references are ignored. 439 """ 440 collation = dict() 441 for ref in [_ for _ in references if _.count_books() == 1]: 442 v1 = ref.ranges[0].start 443 if v1.library not in collation: 444 collation[v1.library] = dict() 445 if v1.book not in collation[v1.library]: 446 collation[v1.library][v1.book] = dict() 447 if v1.chapter not in collation[v1.library][v1.book]: 448 collation[v1.library][v1.book][v1.chapter] = list() 449 collation[v1.library][v1.book][v1.chapter].append(ref) 450 return collation 451 452 def collate_verse_references( 453 self, references: list[Reference] 454 ) -> list[ 455 tuple[ 456 Library, list[tuple[Book, list[tuple[Number, list[tuple[str, Reference]]]]]] 457 ] 458 ]: 459 """ 460 Collate references into nested library, book, chapter and verse-range 461 dictionaries for easy looping. 462 463 Example: 464 ``` 465 for library, book_collation in __.collate_verse_references(refs): 466 for book, chapter_collation in book_collation: 467 for chapter_number, verse_collation in chapter_collation: 468 for verse_numbers, references in verse_collation: 469 # ... 470 ``` 471 472 Note: 473 - verse_numbers is a unique string representing the verse ranges, 474 e.g. '1, 5-6, 13' 475 """ 476 library_list = list() 477 for library_id, books_dict in self.collate_by_verse(references).items(): 478 book_list = list() 479 for book_id, chapters_dict in books_dict.items(): 480 chapter_list = list() 481 for chapter_id, verses_dict in chapters_dict.items(): 482 verse_list = list() 483 for ref_numbers, references in verses_dict.items(): 484 verse_list.append(tuple([ref_numbers, references])) 485 chapter_list.append(tuple([chapter_id, verse_list])) 486 book_list.append(tuple([self.books[library_id, book_id], chapter_list])) 487 library_list.append(tuple([self.libraries[library_id], book_list])) 488 return library_list 489 490 def collate_by_verse( 491 self, references: list[Reference], aggregate_function=None 492 ) -> dict[Number, dict[Number, dict[Number, dict[str, list[Reference]]]]]: 493 """ 494 A collation groups single-book references by library, book, chapter 495 number, and a string reresenting the verse ranges. Multi-book 496 references are ignored. 497 """ 498 collation = dict() 499 for ref in [_ for _ in references if _.count_books() == 1]: 500 v1 = ref.ranges[0].start 501 if v1.library not in collation: 502 collation[v1.library] = dict() 503 if v1.book not in collation[v1.library]: 504 collation[v1.library][v1.book] = dict() 505 if v1.chapter not in collation[v1.library][v1.book]: 506 collation[v1.library][v1.book][v1.chapter] = dict() 507 ref_numbers = self.numbers(ref) 508 if ref_numbers not in collation[v1.library][v1.book][v1.chapter]: 509 collation[v1.library][v1.book][v1.chapter][ref_numbers] = list() 510 collation[v1.library][v1.book][v1.chapter][ref_numbers].append(ref) 511 return collation 512 513 # ----------------------------------- 514 # Matching functions 515 # ----------------------------------- 516 517 def first_reference(self, text: str) -> tuple[str | None, Reference | None]: 518 """ 519 Return the first tuple of (match_str, reference) found by 520 `refspy.manager.Manager.generate_references()` 521 """ 522 generator = self.matcher.generate_references(text) 523 return next(generator, (None, None)) 524 525 def find_references( 526 self, 527 text: str, 528 include_books: bool = False, 529 include_nones: bool = False, 530 use_context: bool = True, 531 ) -> list[tuple[str, Reference | None]]: 532 """ 533 Return a list of tuples of (match_str, reference) found by 534 `refspy.manager.Manager.generate_references()` 535 """ 536 generator = self.matcher.generate_references( 537 text, include_books, include_nones, use_context 538 ) 539 return list(generator) 540 541 def generate_references( 542 self, 543 text: str, 544 yield_books: bool = False, 545 yield_nones: bool = False, 546 use_context: bool = True, 547 ) -> Generator[tuple[str, Reference | None], None, None]: 548 """ 549 Generate tuples of (match_str, reference) for provided text.manager 550 551 Task delegated to `refspy.matcher.Matcher.generate_references()`. 552 553 Args: 554 text: a string to search for references 555 yield_books: Whether to yield book names without reference numbers 556 yield_nones: Whether to yield (match_str, None) for malformed 557 references. 558 use_context: Whether to yield anything other than exact book and 559 number matches 560 561 Yield: 562 A tuple of `(match_str, reference)` for each valid reference. 563 """ 564 yield from self.matcher.generate_references( 565 text, yield_books, yield_nones, use_context 566 ) 567 568 # ----------------------------------- 569 # Reference creator functions 570 # ----------------------------------- 571 572 def bcv( 573 self, 574 alias: str, 575 c: Number | None = None, 576 v: Number | None = None, 577 v_end: Number | None = None, 578 ) -> Reference: 579 """Construct a reference from a book alias and chapter/verse numbers. 580 581 Omitting optional arguments will construct a reference to a whole book 582 or chapter. 583 584 Args: 585 v_end: constructs a verse range from `v` to `v_end`. 586 587 Raises: 588 ValueError: If the book_alias is missing, or the number values are 589 out of range. 590 """ 591 if alias not in self.book_aliases: 592 raise ValueError(f'Book alias "{alias}" not found.') 593 library_id, book_id = self.book_aliases[alias] 594 ta = TypeAdapter(Number) 595 if c is None: 596 return book_reference(library_id, book_id) 597 if v is None: 598 return chapter_reference(library_id, book_id, ta.validate_python(c)) 599 600 if not v_end: 601 return verse_reference( 602 library_id, 603 book_id, 604 ta.validate_python(c), 605 ta.validate_python(v), 606 ) 607 else: 608 return reference( 609 range( 610 verse( 611 library_id, 612 book_id, 613 ta.validate_python(c), 614 ta.validate_python(v), 615 ), 616 verse( 617 library_id, 618 book_id, 619 ta.validate_python(c), 620 ta.validate_python(v_end), 621 ), 622 ) 623 ) 624 625 def bcr( 626 self, alias: str, c: Number, v_ranges: list[Number | tuple[Number, Number]] 627 ) -> Reference: 628 if alias not in self.book_aliases: 629 raise ValueError(f'Book alias "{alias}" not found.') 630 library_id, book_id = self.book_aliases[alias] 631 ranges = [] 632 for val in v_ranges: 633 if isinstance(val, tuple): 634 v_start, v_end = val 635 ranges.append( 636 range( 637 verse(library_id, book_id, c, v_start), 638 verse(library_id, book_id, c, v_end), 639 ) 640 ) 641 elif isinstance(val, int): 642 v_start = v_end = val 643 ranges.append( 644 range( 645 verse(library_id, book_id, c, v_start), 646 verse(library_id, book_id, c, v_end), 647 ) 648 ) 649 return reference(*ranges) 650 651 def r(self, text: str) -> Reference | None: 652 """Return the first matching reference in the given text. 653 654 Book names and malformed references are not matched. 655 """ 656 _, reference = self.first_reference(text) 657 return reference 658 659 # ----------------------------------- 660 # Navigation functions 661 # ----------------------------------- 662 663 def next_chapter(self, ref: Reference) -> Reference | None: 664 """Get the next chapter to the one containing this reference. 665 666 This loops over the end of books using `refspy.models.book.Book.chapters`. 667 This does not loop beyond the end of libraries. 668 """ 669 return self.navigator.next_chapter(ref) 670 671 def prev_chapter(self, ref: Reference) -> Reference | None: 672 """Get the previous chapter to the one containing this reference. 673 674 This loops over the end of books using `refspy.models.book.Book.chapters`, but 675 not beyond the end of libraries. 676 """ 677 return self.navigator.prev_chapter(ref) 678 679 # ----------------------------------- 680 # Manipulation functions 681 # ----------------------------------- 682 683 def get_book(self, ref: Reference) -> Book: 684 """Get the book object for this reference's first range.""" 685 v1 = ref.ranges[0].start 686 return self.books[v1.library, v1.book] 687 688 def book_reference(self, ref: Reference) -> Reference: 689 """Create a reference to the book containing this reference's first 690 range.""" 691 v1 = ref.ranges[0].start 692 return reference( 693 range( 694 verse(v1.library, v1.book, 1, 1), 695 verse(v1.library, v1.book, 999, 999), 696 ) 697 ) 698 699 def chapter_reference(self, ref: Reference) -> Reference: 700 """Create a reference to the chapter containing this reference's first 701 range.""" 702 v1 = ref.ranges[0].start 703 return reference( 704 range( 705 verse(v1.library, v1.book, v1.chapter, 1), 706 verse(v1.library, v1.book, v1.chapter, 999), 707 ) 708 ) 709 710 # ----------------------------------- 711 # Formatting functions 712 # ----------------------------------- 713 714 def link(self, ref: Reference) -> str: 715 """Format a URL Link, with English style number references""" 716 return self.formatter.format(ref, self.formatter.link_format()) 717 718 def name(self, ref: Reference) -> str: 719 """Format a reference.""" 720 return self.formatter.format(ref, self.formatter.name_format(self.language)) 721 722 def book(self, ref: Reference) -> str: 723 """Format a reference using only the book part of its name.""" 724 return self.formatter.format(ref, self.formatter.book_format(self.language)) 725 726 def abbrev_name(self, ref: Reference) -> str: 727 """Format an abbreviated reference.""" 728 return self.formatter.format( 729 ref, self.formatter.abbrev_name_format(self.language) 730 ) 731 732 def abbrev_book(self, ref: Reference) -> str: 733 """Format an abbreviated reference using only the book part of its name.""" 734 return self.formatter.format( 735 ref, self.formatter.abbrev_book_format(self.language) 736 ) 737 738 def numbers(self, ref: Reference) -> str: 739 """Format a reference using only the number part of its name.""" 740 return self.formatter.format(ref, self.formatter.number_format(self.language)) 741 742 def abbrev_numbers(self, ref: Reference) -> str: 743 """Format an abbreviated reference using only the number part of its name.""" 744 return self.formatter.format( 745 ref, self.formatter.abbrev_number_format(self.language) 746 ) 747 748 # ----------------------------------- 749 # Template formatting functions 750 # ----------------------------------- 751 752 def template(self, reference: Reference | None, pattern: str | None = None) -> str: 753 """ 754 Substitute formatting values in a string: 755 756 * `{LINK}` -> "1%20Cor%202:3-4" 757 - like ESC_ABBREV_NAME, but with English numbering in any language. 758 - useful for BibleGateway and presumably other sites. 759 * `{NAME}` -> "1 Corinthians 2:3–4" 760 * `{BOOK}` -> "1 Corinthians" 761 * `{NUMBERS}` -> "2:3–4" 762 * `{ABBREV_NAME}` -> "1 Cor 2:3–4" 763 * `{ABBREV_BOOK}` -> "1 Cor" 764 * `{ABBREV_NUMBERS}` -> "2:3–4" 765 * `{ESC_NAME}` -> "1%20Corinthians%202%3A3-4" 766 * `{ESC_BOOK}` -> "1%20Corinthians" 767 * `{ESC_NUMBERS}` -> "2%3A3-4" 768 * `{ESC_ABBREV_NAME}` -> "1%20Cor%202%3A3-4" 769 * `{ESC_ABBREV_BOOK}` -> "1%20Cor" 770 * `{ESC_ABBREV_NUMBERS}` -> "2%3A3-4" 771 * `{PARAM_NAME}`-> "1cor+2.3-4" 772 * `{PARAM_BOOK}` -> "1cor" 773 * `{PARAM_NUMBERS}` -> "2.3-4" 774 775 For efficiency, we calculate only the values required by the template 776 string. 777 """ 778 if reference is None: 779 return "" 780 781 out = pattern if pattern else "{ABBREV_NAME}" 782 783 regexp = re.compile(r"\{[A-Z_]+\}") 784 matches = regexp.findall(out) 785 numbers = self.numbers(reference) 786 for _ in matches: 787 if _ == "{LINK}": 788 out = out.replace("{LINK}", url_escape(self.link(reference))) 789 elif _ == "{NAME}": 790 out = out.replace("{NAME}", self.name(reference)) 791 elif _ == "{BOOK}": 792 out = out.replace("{BOOK}", self.book(reference)) 793 elif _ == "{NUMBERS}": 794 out = out.replace("{NUMBERS}", numbers) 795 elif _ == "{ASCII_NUMBERS}": 796 out = out.replace("{ASCII_NUMBERS}", numbers.replace("–", "-")) 797 elif _ == "{ABBREV_NAME}": 798 out = out.replace("{ABBREV_NAME}", self.abbrev_name(reference)) 799 elif _ == "{ABBREV_BOOK}": 800 out = out.replace("{ABBREV_BOOK}", self.abbrev_name(reference)) 801 elif _ == "{ESC_NAME}": 802 out = out.replace("{ESC_NAME}", url_escape(self.name(reference))) 803 elif _ == "{ESC_BOOK}": 804 out = out.replace("{ESC_BOOK}", url_escape(self.book(reference))) 805 elif _ == "{ESC_NUMBERS}": 806 out = out.replace("{ESC_NUMBERS}", url_escape(numbers)) 807 elif _ == "{ESC_ASCII_NUMBERS}": 808 out = out.replace( 809 "{ESC_ASCII_NUMBERS}", 810 url_escape(numbers.replace("–", "-")), 811 ) 812 elif _ == "{ESC_ABBREV_NAME}": 813 out = out.replace( 814 "{ESC_ABBREV_NAME}", url_escape(self.abbrev_name(reference)) 815 ) 816 elif _ == "{ESC_ABBREV_BOOK}": 817 out = out.replace( 818 "{ESC_ABBREV_BOOK}", 819 url_escape(self.abbrev_book(reference)), 820 ) 821 elif _ == "{PARAM_NAME}": 822 out = out.replace("{PARAM_NAME}", url_param(self.name(reference))) 823 elif _ == "{PARAM_BOOK}": 824 out = out.replace("{PARAM_BOOK}", url_param(numbers)) 825 elif _ == "{PARAM_NUMBERS}": 826 out = out.replace("{PARAM_NUMBERS}", url_param(numbers)) 827 return out
48class Manager: 49 """ 50 The manager object integrates the main library features into a single 51 convenient facade. 52 """ 53 54 def __init__( 55 self, 56 libraries: list[Library], 57 language: Language, 58 syntax: Syntax | None = None, 59 include_two_letter_aliases: bool = True, 60 ): 61 """ 62 Construct a new Manager object. 63 64 Args: 65 libraries: A list of libraries like NT, OT. 66 language: A language object like ENGLISH. 67 language: A syntax object like INTERNATIONAL. 68 include_two_letter_aliases: Whether to allow `len(alias) == 2` 69 (default: True) 70 """ 71 self.libraries: dict[Number, Library] = index_libraries(libraries) 72 """A lookup dictionary for Libraries by library.id """ 73 74 self.books: dict[tuple[Number, Number], Book] = index_books(libraries) 75 """A lookup dictionary for Books by (library.id, book.id)""" 76 77 self.book_aliases: dict[str, tuple[Number, Number]] = index_book_aliases( 78 libraries, include_two_letter_aliases=include_two_letter_aliases 79 ) 80 """A lookup dictionary for (library.id, book.id) by book alias strings.""" 81 82 self.language: Language = language 83 """Language-specific program data.""" 84 85 self.syntax: Syntax = syntax or language.syntax 86 """Syntax-specific program data.""" 87 88 self.matcher: Matcher = Matcher( 89 self.books, self.book_aliases, self.language, self.syntax 90 ) 91 """Delegate reference matching tasks.""" 92 93 self.formatter: Formatter = Formatter(self.books, self.book_aliases) 94 """Delegate formatting tasks.""" 95 96 self.navigator: Navigator = Navigator(self.books, self.book_aliases) 97 """Delegate navigation tasks.""" 98 99 # ----------------------------------- 100 # Index and summary functions 101 # ----------------------------------- 102 103 def make_index_references_by_chapter( 104 self, references: list[Reference] 105 ) -> list[Reference]: 106 """Return a sorted, combined, simplified list of references by book.""" 107 collation = self.collate_chapter_references( 108 sorted([ref for ref in references if ref and not ref.is_book()]) 109 ) 110 indexes = [] 111 for _, book_collation in collation: 112 for _, chapter_collation in book_collation: 113 for _, reference_list in chapter_collation: 114 compact_ref = join_references(reference_list) 115 indexes.append(compact_ref) 116 return indexes 117 118 def make_index( 119 self, 120 references: list[Reference], 121 pattern: str | None = None, 122 ) -> str | None: 123 """ 124 Return a one-line list of sorted references. No merging or combining. 125 126 Args: 127 pattern: a formatting string suitable for links, see 128 `refspy.manager.Manager.template()`. 129 130 Note: 131 If no template pattern is provided, reference formatting 132 will default to `refspy.manager.Manager.abbrev_name()` 133 """ 134 if indexes := self.make_index_references_by_chapter(references): 135 return self.template(join_references(indexes)) 136 else: 137 return None 138 139 def make_summary_references(self, references: list[Reference]) -> list[Reference]: 140 """Return a sorted, combined, simplified list of References.""" 141 collation = self.collate( 142 sorted([ref for ref in references if ref and not ref.is_book()]) 143 ) 144 summary = [] 145 for _, book_collation in collation: 146 for _, reference_list in book_collation: 147 compact_ref = self.combine_references(reference_list) 148 summary.append(compact_ref) 149 return summary 150 151 def make_summary( 152 self, references: list[Reference], pattern: str | None = None 153 ) -> str | None: 154 """ 155 Return a string showing a sorted, combined, list of References. 156 157 Args: 158 pattern: a formatting string suitable for links, see 159 `refspy.manager.Manager.template()`. 160 161 Note: 162 If no template pattern is provided, reference formatting 163 will default to `refspy.manager.Manager.abbrev_name()`. 164 165 Args: 166 max_chapters: The maximum number of chapter hotspots to return. 167 min_references: The minimal references per chapter that qualifies as a hotspot. 168 """ 169 if summaries := self.make_summary_references(references): 170 return "; ".join([self.template(ref, pattern) for ref in summaries]) 171 else: 172 return None 173 174 def make_summary_references_by_chapter( 175 self, references: list[Reference] 176 ) -> list[Reference]: 177 """Return a sorted, combined, simplified list of references by book.""" 178 collation = self.collate_chapter_references( 179 sorted([ref for ref in references if ref and not ref.is_book()]) 180 ) 181 summary = [] 182 for _, book_collation in collation: 183 for _, chapter_collation in book_collation: 184 for _, reference_list in chapter_collation: 185 compact_ref = self.combine_references(reference_list) 186 summary.append(compact_ref) 187 return summary 188 189 def make_summary_by_chapter( 190 self, references: list[Reference], pattern: str | None = None 191 ) -> str | None: 192 """ 193 Return a string showing a sorted, combined, list of References. 194 195 Args: 196 pattern: a formatting string suitable for links, see 197 `refspy.manager.Manager.template()`. 198 199 Note: 200 If no template pattern is provided, reference formatting 201 will default to `refspy.manager.Manager.abbrev_name()`. 202 203 Args: 204 a max_chapters: The maximum number of chapter hotspots to return. 205 min_references: The minimal references per chapter that qualifies as a hotspot. 206 """ 207 if summaries := self.make_summary_references_by_chapter(references): 208 return ", ".join([self.template(ref, pattern) for ref in summaries]) 209 else: 210 return None 211 212 def make_hotspot_tuples( 213 self, 214 references: list[Reference], 215 max_chapters: int = 7, 216 min_references: int = 2, 217 ) -> list[tuple[Reference, int]]: 218 """ 219 Find the most referenced chapters in a set of references. 220 221 Args: 222 max_chapters: The maximum number of chapter hotspots to return. 223 min_references: The minimal references per chapter that qualifies as a hotspot. 224 225 Return: 226 a list of tuples of (chapter reference, count) in descending frequency 227 """ 228 if not references: 229 return [] 230 totals = dict() 231 for ref in sort_references(references): 232 for _ in ref.ranges: 233 for tuple in [ 234 (_.start.library, _.start.book, _.start.chapter), 235 (_.end.library, _.end.book, _.end.chapter), 236 ]: 237 if tuple in totals: 238 totals[tuple] += 1 239 else: 240 totals[tuple] = 1 241 hotspots = [ 242 (chapter_reference(*tuple), int(total / 2)) 243 for tuple, total in totals.items() 244 if total / 2 >= min_references 245 ] 246 hotspots_desc = sorted(hotspots, key=lambda item: item[1], reverse=True) 247 return hotspots_desc[:max_chapters] 248 249 def make_hotspot_references( 250 self, 251 references: list[Reference], 252 max_chapters: int = 7, 253 min_references: int = 2, 254 ) -> list[Reference]: 255 """ 256 Return chapter references in descending order of frequency. 257 258 Args: 259 max_chapters: The maximum number of chapter hotspots to return. 260 min_references: The minimal references per chapter that qualifies as a hotspot. 261 """ 262 tuples = self.make_hotspot_tuples(references, max_chapters, min_references) 263 return [ref for ref, _ in tuples] 264 265 def make_hotspots( 266 self, 267 references: list[Reference], 268 max_chapters: int = 7, 269 min_references: int = 2, 270 pattern: str | None = None, 271 ) -> str | None: 272 """ 273 Return chapter references in descending order of frequency, as a text 274 string, e.g. "Rom 3, 1 Cor 6", or None. 275 276 Args: 277 max_chapters: The maximum number of chapter hotspots to return. 278 min_references: The minimal references per chapter that qualifies as a hotspot. 279 pattern: an optional template pattern for formatting references 280 """ 281 if refs := self.make_hotspot_references( 282 references, max_chapters=max_chapters, min_references=min_references 283 ): 284 return ", ".join([self.template(ref, pattern) for ref in refs]) 285 else: 286 return None 287 288 # ----------------------------------- 289 # Merging functions 290 # ----------------------------------- 291 292 def sort(self, references: list[Reference]) -> list[Reference]: 293 """Sort a list of references by comparing its lists of ranges""" 294 return sort_references(references) 295 296 def unique(self, references: list[Reference]) -> list[Reference]: 297 """Return unique entries in an already sorted list. 298 299 Note: 300 - Like `uniq` on UNIX. 301 """ 302 return unique_references(references) 303 304 def split(self, reference: Reference) -> list[Reference]: 305 """Split a reference into a list, with one range per reference 306 307 TODO: 308 - Add split by book, chapter? 309 """ 310 return split_reference(reference) 311 312 def join(self, references: list[Reference]) -> Reference: 313 """Join a list of references into a single reference 314 315 TODO: 316 - Add join by book, chapter? 317 """ 318 return join_references(references) 319 320 def sort_references(self, references: list[Reference]) -> Reference: 321 """ 322 DEPRECATED for unclear naming: will be removed before 1.0 323 324 Return a single reference containing sorted ranges from a list of 325 references. 326 327 Superceded by `join_references(sort_references(references))` or just 328 `__.join(__.sort(references))` in Manager. 329 """ 330 all_ranges = [_ for ref in references for _ in ref.ranges] 331 return Reference(ranges=sorted(all_ranges)) 332 333 def merge_references(self, references: list[Reference]) -> Reference: 334 """For a list of references, merge their ranges into a new reference. 335 336 Merging means combining ranges that overlap. 337 """ 338 ranges = [] 339 for ref in references: 340 ranges.extend(ref.ranges) 341 new_ref = reference(*merge_ranges(ranges)) 342 return new_ref 343 344 def combine_references(self, references: list[Reference]) -> Reference: 345 """For a list of references, combine their ranges into a new reference. 346 347 Combining means sorting ranges, merging overlaps, and joining adjacent 348 records. 349 """ 350 ranges = [] 351 for ref in references: 352 ranges.extend(ref.ranges) 353 new_ref = reference(*combine_ranges(ranges)) 354 return new_ref 355 356 # ----------------------------------- 357 # Iteration functions 358 # ----------------------------------- 359 360 def collate( 361 self, references: list[Reference] 362 ) -> list[tuple[Library, list[tuple[Book, list[Reference]]]]]: 363 """ 364 The default collation is by book; use collate_chapter_references 365 otherwise. 366 """ 367 return self.collate_book_references(references) 368 369 def collate_book_references( 370 self, references: list[Reference] 371 ) -> list[tuple[Library, list[tuple[Book, list[Reference]]]]]: 372 """ 373 Collate references into nested library and book dictionaries for easy 374 looping. 375 376 Example: 377 ``` 378 for library, book_collation in __.collate_books_references(refs): 379 for book, references in book_collation: 380 # ... 381 ``` 382 """ 383 library_list = list() 384 for library_id, books_dict in self.collate_by_book(references).items(): 385 book_list = list() 386 for book_id, references in books_dict.items(): 387 book_list.append(tuple([self.books[library_id, book_id], references])) 388 library_list.append(tuple([self.libraries[library_id], book_list])) 389 return library_list 390 391 def collate_by_book( 392 self, references: list[Reference] 393 ) -> dict[Number, dict[Number, list[Reference]]]: 394 """ 395 A collation groups single-book references by library and book IDs. 396 Multi-book references are ignored. 397 """ 398 collation = dict() 399 for ref in [_ for _ in references if _.count_books() == 1]: 400 v1 = ref.ranges[0].start 401 if v1.library not in collation: 402 collation[v1.library] = dict() 403 if v1.book not in collation[v1.library]: 404 collation[v1.library][v1.book] = list() 405 collation[v1.library][v1.book].append(ref) 406 return collation 407 408 def collate_chapter_references( 409 self, references: list[Reference] 410 ) -> list[tuple[Library, list[tuple[Book, list[tuple[Number, list[Reference]]]]]]]: 411 """ 412 Collate references into nested library, book, and chapter dictionaries 413 for easy looping. 414 415 Example: 416 ``` 417 for library, book_collation in __.collate_chapter_references(refs): 418 for book, chapter_collation in book_collation: 419 for chapter_number, references in chapter_collation: 420 # ... 421 ``` 422 """ 423 library_list = list() 424 for library_id, books_dict in self.collate_by_chapter(references).items(): 425 book_list = list() 426 for book_id, chapters_dict in books_dict.items(): 427 chapter_list = list() 428 for chapter_id, references in chapters_dict.items(): 429 chapter_list.append(tuple([chapter_id, references])) 430 book_list.append(tuple([self.books[library_id, book_id], chapter_list])) 431 library_list.append(tuple([self.libraries[library_id], book_list])) 432 return library_list 433 434 def collate_by_chapter( 435 self, references: list[Reference] 436 ) -> dict[Number, dict[Number, dict[Number, list[Reference]]]]: 437 """ 438 A collation groups single-book references by library, book, and chapter 439 IDs. Multi-book references are ignored. 440 """ 441 collation = dict() 442 for ref in [_ for _ in references if _.count_books() == 1]: 443 v1 = ref.ranges[0].start 444 if v1.library not in collation: 445 collation[v1.library] = dict() 446 if v1.book not in collation[v1.library]: 447 collation[v1.library][v1.book] = dict() 448 if v1.chapter not in collation[v1.library][v1.book]: 449 collation[v1.library][v1.book][v1.chapter] = list() 450 collation[v1.library][v1.book][v1.chapter].append(ref) 451 return collation 452 453 def collate_verse_references( 454 self, references: list[Reference] 455 ) -> list[ 456 tuple[ 457 Library, list[tuple[Book, list[tuple[Number, list[tuple[str, Reference]]]]]] 458 ] 459 ]: 460 """ 461 Collate references into nested library, book, chapter and verse-range 462 dictionaries for easy looping. 463 464 Example: 465 ``` 466 for library, book_collation in __.collate_verse_references(refs): 467 for book, chapter_collation in book_collation: 468 for chapter_number, verse_collation in chapter_collation: 469 for verse_numbers, references in verse_collation: 470 # ... 471 ``` 472 473 Note: 474 - verse_numbers is a unique string representing the verse ranges, 475 e.g. '1, 5-6, 13' 476 """ 477 library_list = list() 478 for library_id, books_dict in self.collate_by_verse(references).items(): 479 book_list = list() 480 for book_id, chapters_dict in books_dict.items(): 481 chapter_list = list() 482 for chapter_id, verses_dict in chapters_dict.items(): 483 verse_list = list() 484 for ref_numbers, references in verses_dict.items(): 485 verse_list.append(tuple([ref_numbers, references])) 486 chapter_list.append(tuple([chapter_id, verse_list])) 487 book_list.append(tuple([self.books[library_id, book_id], chapter_list])) 488 library_list.append(tuple([self.libraries[library_id], book_list])) 489 return library_list 490 491 def collate_by_verse( 492 self, references: list[Reference], aggregate_function=None 493 ) -> dict[Number, dict[Number, dict[Number, dict[str, list[Reference]]]]]: 494 """ 495 A collation groups single-book references by library, book, chapter 496 number, and a string reresenting the verse ranges. Multi-book 497 references are ignored. 498 """ 499 collation = dict() 500 for ref in [_ for _ in references if _.count_books() == 1]: 501 v1 = ref.ranges[0].start 502 if v1.library not in collation: 503 collation[v1.library] = dict() 504 if v1.book not in collation[v1.library]: 505 collation[v1.library][v1.book] = dict() 506 if v1.chapter not in collation[v1.library][v1.book]: 507 collation[v1.library][v1.book][v1.chapter] = dict() 508 ref_numbers = self.numbers(ref) 509 if ref_numbers not in collation[v1.library][v1.book][v1.chapter]: 510 collation[v1.library][v1.book][v1.chapter][ref_numbers] = list() 511 collation[v1.library][v1.book][v1.chapter][ref_numbers].append(ref) 512 return collation 513 514 # ----------------------------------- 515 # Matching functions 516 # ----------------------------------- 517 518 def first_reference(self, text: str) -> tuple[str | None, Reference | None]: 519 """ 520 Return the first tuple of (match_str, reference) found by 521 `refspy.manager.Manager.generate_references()` 522 """ 523 generator = self.matcher.generate_references(text) 524 return next(generator, (None, None)) 525 526 def find_references( 527 self, 528 text: str, 529 include_books: bool = False, 530 include_nones: bool = False, 531 use_context: bool = True, 532 ) -> list[tuple[str, Reference | None]]: 533 """ 534 Return a list of tuples of (match_str, reference) found by 535 `refspy.manager.Manager.generate_references()` 536 """ 537 generator = self.matcher.generate_references( 538 text, include_books, include_nones, use_context 539 ) 540 return list(generator) 541 542 def generate_references( 543 self, 544 text: str, 545 yield_books: bool = False, 546 yield_nones: bool = False, 547 use_context: bool = True, 548 ) -> Generator[tuple[str, Reference | None], None, None]: 549 """ 550 Generate tuples of (match_str, reference) for provided text.manager 551 552 Task delegated to `refspy.matcher.Matcher.generate_references()`. 553 554 Args: 555 text: a string to search for references 556 yield_books: Whether to yield book names without reference numbers 557 yield_nones: Whether to yield (match_str, None) for malformed 558 references. 559 use_context: Whether to yield anything other than exact book and 560 number matches 561 562 Yield: 563 A tuple of `(match_str, reference)` for each valid reference. 564 """ 565 yield from self.matcher.generate_references( 566 text, yield_books, yield_nones, use_context 567 ) 568 569 # ----------------------------------- 570 # Reference creator functions 571 # ----------------------------------- 572 573 def bcv( 574 self, 575 alias: str, 576 c: Number | None = None, 577 v: Number | None = None, 578 v_end: Number | None = None, 579 ) -> Reference: 580 """Construct a reference from a book alias and chapter/verse numbers. 581 582 Omitting optional arguments will construct a reference to a whole book 583 or chapter. 584 585 Args: 586 v_end: constructs a verse range from `v` to `v_end`. 587 588 Raises: 589 ValueError: If the book_alias is missing, or the number values are 590 out of range. 591 """ 592 if alias not in self.book_aliases: 593 raise ValueError(f'Book alias "{alias}" not found.') 594 library_id, book_id = self.book_aliases[alias] 595 ta = TypeAdapter(Number) 596 if c is None: 597 return book_reference(library_id, book_id) 598 if v is None: 599 return chapter_reference(library_id, book_id, ta.validate_python(c)) 600 601 if not v_end: 602 return verse_reference( 603 library_id, 604 book_id, 605 ta.validate_python(c), 606 ta.validate_python(v), 607 ) 608 else: 609 return reference( 610 range( 611 verse( 612 library_id, 613 book_id, 614 ta.validate_python(c), 615 ta.validate_python(v), 616 ), 617 verse( 618 library_id, 619 book_id, 620 ta.validate_python(c), 621 ta.validate_python(v_end), 622 ), 623 ) 624 ) 625 626 def bcr( 627 self, alias: str, c: Number, v_ranges: list[Number | tuple[Number, Number]] 628 ) -> Reference: 629 if alias not in self.book_aliases: 630 raise ValueError(f'Book alias "{alias}" not found.') 631 library_id, book_id = self.book_aliases[alias] 632 ranges = [] 633 for val in v_ranges: 634 if isinstance(val, tuple): 635 v_start, v_end = val 636 ranges.append( 637 range( 638 verse(library_id, book_id, c, v_start), 639 verse(library_id, book_id, c, v_end), 640 ) 641 ) 642 elif isinstance(val, int): 643 v_start = v_end = val 644 ranges.append( 645 range( 646 verse(library_id, book_id, c, v_start), 647 verse(library_id, book_id, c, v_end), 648 ) 649 ) 650 return reference(*ranges) 651 652 def r(self, text: str) -> Reference | None: 653 """Return the first matching reference in the given text. 654 655 Book names and malformed references are not matched. 656 """ 657 _, reference = self.first_reference(text) 658 return reference 659 660 # ----------------------------------- 661 # Navigation functions 662 # ----------------------------------- 663 664 def next_chapter(self, ref: Reference) -> Reference | None: 665 """Get the next chapter to the one containing this reference. 666 667 This loops over the end of books using `refspy.models.book.Book.chapters`. 668 This does not loop beyond the end of libraries. 669 """ 670 return self.navigator.next_chapter(ref) 671 672 def prev_chapter(self, ref: Reference) -> Reference | None: 673 """Get the previous chapter to the one containing this reference. 674 675 This loops over the end of books using `refspy.models.book.Book.chapters`, but 676 not beyond the end of libraries. 677 """ 678 return self.navigator.prev_chapter(ref) 679 680 # ----------------------------------- 681 # Manipulation functions 682 # ----------------------------------- 683 684 def get_book(self, ref: Reference) -> Book: 685 """Get the book object for this reference's first range.""" 686 v1 = ref.ranges[0].start 687 return self.books[v1.library, v1.book] 688 689 def book_reference(self, ref: Reference) -> Reference: 690 """Create a reference to the book containing this reference's first 691 range.""" 692 v1 = ref.ranges[0].start 693 return reference( 694 range( 695 verse(v1.library, v1.book, 1, 1), 696 verse(v1.library, v1.book, 999, 999), 697 ) 698 ) 699 700 def chapter_reference(self, ref: Reference) -> Reference: 701 """Create a reference to the chapter containing this reference's first 702 range.""" 703 v1 = ref.ranges[0].start 704 return reference( 705 range( 706 verse(v1.library, v1.book, v1.chapter, 1), 707 verse(v1.library, v1.book, v1.chapter, 999), 708 ) 709 ) 710 711 # ----------------------------------- 712 # Formatting functions 713 # ----------------------------------- 714 715 def link(self, ref: Reference) -> str: 716 """Format a URL Link, with English style number references""" 717 return self.formatter.format(ref, self.formatter.link_format()) 718 719 def name(self, ref: Reference) -> str: 720 """Format a reference.""" 721 return self.formatter.format(ref, self.formatter.name_format(self.language)) 722 723 def book(self, ref: Reference) -> str: 724 """Format a reference using only the book part of its name.""" 725 return self.formatter.format(ref, self.formatter.book_format(self.language)) 726 727 def abbrev_name(self, ref: Reference) -> str: 728 """Format an abbreviated reference.""" 729 return self.formatter.format( 730 ref, self.formatter.abbrev_name_format(self.language) 731 ) 732 733 def abbrev_book(self, ref: Reference) -> str: 734 """Format an abbreviated reference using only the book part of its name.""" 735 return self.formatter.format( 736 ref, self.formatter.abbrev_book_format(self.language) 737 ) 738 739 def numbers(self, ref: Reference) -> str: 740 """Format a reference using only the number part of its name.""" 741 return self.formatter.format(ref, self.formatter.number_format(self.language)) 742 743 def abbrev_numbers(self, ref: Reference) -> str: 744 """Format an abbreviated reference using only the number part of its name.""" 745 return self.formatter.format( 746 ref, self.formatter.abbrev_number_format(self.language) 747 ) 748 749 # ----------------------------------- 750 # Template formatting functions 751 # ----------------------------------- 752 753 def template(self, reference: Reference | None, pattern: str | None = None) -> str: 754 """ 755 Substitute formatting values in a string: 756 757 * `{LINK}` -> "1%20Cor%202:3-4" 758 - like ESC_ABBREV_NAME, but with English numbering in any language. 759 - useful for BibleGateway and presumably other sites. 760 * `{NAME}` -> "1 Corinthians 2:3–4" 761 * `{BOOK}` -> "1 Corinthians" 762 * `{NUMBERS}` -> "2:3–4" 763 * `{ABBREV_NAME}` -> "1 Cor 2:3–4" 764 * `{ABBREV_BOOK}` -> "1 Cor" 765 * `{ABBREV_NUMBERS}` -> "2:3–4" 766 * `{ESC_NAME}` -> "1%20Corinthians%202%3A3-4" 767 * `{ESC_BOOK}` -> "1%20Corinthians" 768 * `{ESC_NUMBERS}` -> "2%3A3-4" 769 * `{ESC_ABBREV_NAME}` -> "1%20Cor%202%3A3-4" 770 * `{ESC_ABBREV_BOOK}` -> "1%20Cor" 771 * `{ESC_ABBREV_NUMBERS}` -> "2%3A3-4" 772 * `{PARAM_NAME}`-> "1cor+2.3-4" 773 * `{PARAM_BOOK}` -> "1cor" 774 * `{PARAM_NUMBERS}` -> "2.3-4" 775 776 For efficiency, we calculate only the values required by the template 777 string. 778 """ 779 if reference is None: 780 return "" 781 782 out = pattern if pattern else "{ABBREV_NAME}" 783 784 regexp = re.compile(r"\{[A-Z_]+\}") 785 matches = regexp.findall(out) 786 numbers = self.numbers(reference) 787 for _ in matches: 788 if _ == "{LINK}": 789 out = out.replace("{LINK}", url_escape(self.link(reference))) 790 elif _ == "{NAME}": 791 out = out.replace("{NAME}", self.name(reference)) 792 elif _ == "{BOOK}": 793 out = out.replace("{BOOK}", self.book(reference)) 794 elif _ == "{NUMBERS}": 795 out = out.replace("{NUMBERS}", numbers) 796 elif _ == "{ASCII_NUMBERS}": 797 out = out.replace("{ASCII_NUMBERS}", numbers.replace("–", "-")) 798 elif _ == "{ABBREV_NAME}": 799 out = out.replace("{ABBREV_NAME}", self.abbrev_name(reference)) 800 elif _ == "{ABBREV_BOOK}": 801 out = out.replace("{ABBREV_BOOK}", self.abbrev_name(reference)) 802 elif _ == "{ESC_NAME}": 803 out = out.replace("{ESC_NAME}", url_escape(self.name(reference))) 804 elif _ == "{ESC_BOOK}": 805 out = out.replace("{ESC_BOOK}", url_escape(self.book(reference))) 806 elif _ == "{ESC_NUMBERS}": 807 out = out.replace("{ESC_NUMBERS}", url_escape(numbers)) 808 elif _ == "{ESC_ASCII_NUMBERS}": 809 out = out.replace( 810 "{ESC_ASCII_NUMBERS}", 811 url_escape(numbers.replace("–", "-")), 812 ) 813 elif _ == "{ESC_ABBREV_NAME}": 814 out = out.replace( 815 "{ESC_ABBREV_NAME}", url_escape(self.abbrev_name(reference)) 816 ) 817 elif _ == "{ESC_ABBREV_BOOK}": 818 out = out.replace( 819 "{ESC_ABBREV_BOOK}", 820 url_escape(self.abbrev_book(reference)), 821 ) 822 elif _ == "{PARAM_NAME}": 823 out = out.replace("{PARAM_NAME}", url_param(self.name(reference))) 824 elif _ == "{PARAM_BOOK}": 825 out = out.replace("{PARAM_BOOK}", url_param(numbers)) 826 elif _ == "{PARAM_NUMBERS}": 827 out = out.replace("{PARAM_NUMBERS}", url_param(numbers)) 828 return out
The manager object integrates the main library features into a single convenient facade.
54 def __init__( 55 self, 56 libraries: list[Library], 57 language: Language, 58 syntax: Syntax | None = None, 59 include_two_letter_aliases: bool = True, 60 ): 61 """ 62 Construct a new Manager object. 63 64 Args: 65 libraries: A list of libraries like NT, OT. 66 language: A language object like ENGLISH. 67 language: A syntax object like INTERNATIONAL. 68 include_two_letter_aliases: Whether to allow `len(alias) == 2` 69 (default: True) 70 """ 71 self.libraries: dict[Number, Library] = index_libraries(libraries) 72 """A lookup dictionary for Libraries by library.id """ 73 74 self.books: dict[tuple[Number, Number], Book] = index_books(libraries) 75 """A lookup dictionary for Books by (library.id, book.id)""" 76 77 self.book_aliases: dict[str, tuple[Number, Number]] = index_book_aliases( 78 libraries, include_two_letter_aliases=include_two_letter_aliases 79 ) 80 """A lookup dictionary for (library.id, book.id) by book alias strings.""" 81 82 self.language: Language = language 83 """Language-specific program data.""" 84 85 self.syntax: Syntax = syntax or language.syntax 86 """Syntax-specific program data.""" 87 88 self.matcher: Matcher = Matcher( 89 self.books, self.book_aliases, self.language, self.syntax 90 ) 91 """Delegate reference matching tasks.""" 92 93 self.formatter: Formatter = Formatter(self.books, self.book_aliases) 94 """Delegate formatting tasks.""" 95 96 self.navigator: Navigator = Navigator(self.books, self.book_aliases) 97 """Delegate navigation tasks."""
Construct a new Manager object.
Arguments:
- libraries: A list of libraries like NT, OT.
- language: A language object like ENGLISH.
- language: A syntax object like INTERNATIONAL.
- include_two_letter_aliases: Whether to allow
len(alias) == 2(default: True)
A lookup dictionary for Libraries by library.id
A lookup dictionary for Books by (library.id, book.id)
A lookup dictionary for (library.id, book.id) by book alias strings.
103 def make_index_references_by_chapter( 104 self, references: list[Reference] 105 ) -> list[Reference]: 106 """Return a sorted, combined, simplified list of references by book.""" 107 collation = self.collate_chapter_references( 108 sorted([ref for ref in references if ref and not ref.is_book()]) 109 ) 110 indexes = [] 111 for _, book_collation in collation: 112 for _, chapter_collation in book_collation: 113 for _, reference_list in chapter_collation: 114 compact_ref = join_references(reference_list) 115 indexes.append(compact_ref) 116 return indexes
Return a sorted, combined, simplified list of references by book.
118 def make_index( 119 self, 120 references: list[Reference], 121 pattern: str | None = None, 122 ) -> str | None: 123 """ 124 Return a one-line list of sorted references. No merging or combining. 125 126 Args: 127 pattern: a formatting string suitable for links, see 128 `refspy.manager.Manager.template()`. 129 130 Note: 131 If no template pattern is provided, reference formatting 132 will default to `refspy.manager.Manager.abbrev_name()` 133 """ 134 if indexes := self.make_index_references_by_chapter(references): 135 return self.template(join_references(indexes)) 136 else: 137 return None
Return a one-line list of sorted references. No merging or combining.
Arguments:
- pattern: a formatting string suitable for links, see
refspy.manager.Manager.template().
Note:
If no template pattern is provided, reference formatting will default to
refspy.manager.Manager.abbrev_name()
139 def make_summary_references(self, references: list[Reference]) -> list[Reference]: 140 """Return a sorted, combined, simplified list of References.""" 141 collation = self.collate( 142 sorted([ref for ref in references if ref and not ref.is_book()]) 143 ) 144 summary = [] 145 for _, book_collation in collation: 146 for _, reference_list in book_collation: 147 compact_ref = self.combine_references(reference_list) 148 summary.append(compact_ref) 149 return summary
Return a sorted, combined, simplified list of References.
151 def make_summary( 152 self, references: list[Reference], pattern: str | None = None 153 ) -> str | None: 154 """ 155 Return a string showing a sorted, combined, list of References. 156 157 Args: 158 pattern: a formatting string suitable for links, see 159 `refspy.manager.Manager.template()`. 160 161 Note: 162 If no template pattern is provided, reference formatting 163 will default to `refspy.manager.Manager.abbrev_name()`. 164 165 Args: 166 max_chapters: The maximum number of chapter hotspots to return. 167 min_references: The minimal references per chapter that qualifies as a hotspot. 168 """ 169 if summaries := self.make_summary_references(references): 170 return "; ".join([self.template(ref, pattern) for ref in summaries]) 171 else: 172 return None
Return a string showing a sorted, combined, list of References.
Arguments:
- pattern: a formatting string suitable for links, see
refspy.manager.Manager.template().
Note:
If no template pattern is provided, reference formatting will default to
refspy.manager.Manager.abbrev_name().
Arguments:
- max_chapters: The maximum number of chapter hotspots to return.
- min_references: The minimal references per chapter that qualifies as a hotspot.
174 def make_summary_references_by_chapter( 175 self, references: list[Reference] 176 ) -> list[Reference]: 177 """Return a sorted, combined, simplified list of references by book.""" 178 collation = self.collate_chapter_references( 179 sorted([ref for ref in references if ref and not ref.is_book()]) 180 ) 181 summary = [] 182 for _, book_collation in collation: 183 for _, chapter_collation in book_collation: 184 for _, reference_list in chapter_collation: 185 compact_ref = self.combine_references(reference_list) 186 summary.append(compact_ref) 187 return summary
Return a sorted, combined, simplified list of references by book.
189 def make_summary_by_chapter( 190 self, references: list[Reference], pattern: str | None = None 191 ) -> str | None: 192 """ 193 Return a string showing a sorted, combined, list of References. 194 195 Args: 196 pattern: a formatting string suitable for links, see 197 `refspy.manager.Manager.template()`. 198 199 Note: 200 If no template pattern is provided, reference formatting 201 will default to `refspy.manager.Manager.abbrev_name()`. 202 203 Args: 204 a max_chapters: The maximum number of chapter hotspots to return. 205 min_references: The minimal references per chapter that qualifies as a hotspot. 206 """ 207 if summaries := self.make_summary_references_by_chapter(references): 208 return ", ".join([self.template(ref, pattern) for ref in summaries]) 209 else: 210 return None
Return a string showing a sorted, combined, list of References.
Args:
pattern: a formatting string suitable for links, see
`refspy.manager.Manager.template()`.
Note:
If no template pattern is provided, reference formatting
will default to `refspy.manager.Manager.abbrev_name()`.
Args:
a max_chapters: The maximum number of chapter hotspots to return. min_references: The minimal references per chapter that qualifies as a hotspot.
212 def make_hotspot_tuples( 213 self, 214 references: list[Reference], 215 max_chapters: int = 7, 216 min_references: int = 2, 217 ) -> list[tuple[Reference, int]]: 218 """ 219 Find the most referenced chapters in a set of references. 220 221 Args: 222 max_chapters: The maximum number of chapter hotspots to return. 223 min_references: The minimal references per chapter that qualifies as a hotspot. 224 225 Return: 226 a list of tuples of (chapter reference, count) in descending frequency 227 """ 228 if not references: 229 return [] 230 totals = dict() 231 for ref in sort_references(references): 232 for _ in ref.ranges: 233 for tuple in [ 234 (_.start.library, _.start.book, _.start.chapter), 235 (_.end.library, _.end.book, _.end.chapter), 236 ]: 237 if tuple in totals: 238 totals[tuple] += 1 239 else: 240 totals[tuple] = 1 241 hotspots = [ 242 (chapter_reference(*tuple), int(total / 2)) 243 for tuple, total in totals.items() 244 if total / 2 >= min_references 245 ] 246 hotspots_desc = sorted(hotspots, key=lambda item: item[1], reverse=True) 247 return hotspots_desc[:max_chapters]
Find the most referenced chapters in a set of references.
Arguments:
- max_chapters: The maximum number of chapter hotspots to return.
- min_references: The minimal references per chapter that qualifies as a hotspot.
Return:
a list of tuples of (chapter reference, count) in descending frequency
249 def make_hotspot_references( 250 self, 251 references: list[Reference], 252 max_chapters: int = 7, 253 min_references: int = 2, 254 ) -> list[Reference]: 255 """ 256 Return chapter references in descending order of frequency. 257 258 Args: 259 max_chapters: The maximum number of chapter hotspots to return. 260 min_references: The minimal references per chapter that qualifies as a hotspot. 261 """ 262 tuples = self.make_hotspot_tuples(references, max_chapters, min_references) 263 return [ref for ref, _ in tuples]
Return chapter references in descending order of frequency.
Arguments:
- max_chapters: The maximum number of chapter hotspots to return.
- min_references: The minimal references per chapter that qualifies as a hotspot.
265 def make_hotspots( 266 self, 267 references: list[Reference], 268 max_chapters: int = 7, 269 min_references: int = 2, 270 pattern: str | None = None, 271 ) -> str | None: 272 """ 273 Return chapter references in descending order of frequency, as a text 274 string, e.g. "Rom 3, 1 Cor 6", or None. 275 276 Args: 277 max_chapters: The maximum number of chapter hotspots to return. 278 min_references: The minimal references per chapter that qualifies as a hotspot. 279 pattern: an optional template pattern for formatting references 280 """ 281 if refs := self.make_hotspot_references( 282 references, max_chapters=max_chapters, min_references=min_references 283 ): 284 return ", ".join([self.template(ref, pattern) for ref in refs]) 285 else: 286 return None
Return chapter references in descending order of frequency, as a text string, e.g. "Rom 3, 1 Cor 6", or None.
Arguments:
- max_chapters: The maximum number of chapter hotspots to return.
- min_references: The minimal references per chapter that qualifies as a hotspot.
- pattern: an optional template pattern for formatting references
292 def sort(self, references: list[Reference]) -> list[Reference]: 293 """Sort a list of references by comparing its lists of ranges""" 294 return sort_references(references)
Sort a list of references by comparing its lists of ranges
296 def unique(self, references: list[Reference]) -> list[Reference]: 297 """Return unique entries in an already sorted list. 298 299 Note: 300 - Like `uniq` on UNIX. 301 """ 302 return unique_references(references)
Return unique entries in an already sorted list.
Note:
- Like
uniqon UNIX.
304 def split(self, reference: Reference) -> list[Reference]: 305 """Split a reference into a list, with one range per reference 306 307 TODO: 308 - Add split by book, chapter? 309 """ 310 return split_reference(reference)
Split a reference into a list, with one range per reference
TODO:
- Add split by book, chapter?
312 def join(self, references: list[Reference]) -> Reference: 313 """Join a list of references into a single reference 314 315 TODO: 316 - Add join by book, chapter? 317 """ 318 return join_references(references)
Join a list of references into a single reference
TODO:
- Add join by book, chapter?
320 def sort_references(self, references: list[Reference]) -> Reference: 321 """ 322 DEPRECATED for unclear naming: will be removed before 1.0 323 324 Return a single reference containing sorted ranges from a list of 325 references. 326 327 Superceded by `join_references(sort_references(references))` or just 328 `__.join(__.sort(references))` in Manager. 329 """ 330 all_ranges = [_ for ref in references for _ in ref.ranges] 331 return Reference(ranges=sorted(all_ranges))
DEPRECATED for unclear naming: will be removed before 1.0
Return a single reference containing sorted ranges from a list of references.
Superceded by join_references(sort_references(references)) or just
__.join(__.sort(references)) in Manager.
333 def merge_references(self, references: list[Reference]) -> Reference: 334 """For a list of references, merge their ranges into a new reference. 335 336 Merging means combining ranges that overlap. 337 """ 338 ranges = [] 339 for ref in references: 340 ranges.extend(ref.ranges) 341 new_ref = reference(*merge_ranges(ranges)) 342 return new_ref
For a list of references, merge their ranges into a new reference.
Merging means combining ranges that overlap.
344 def combine_references(self, references: list[Reference]) -> Reference: 345 """For a list of references, combine their ranges into a new reference. 346 347 Combining means sorting ranges, merging overlaps, and joining adjacent 348 records. 349 """ 350 ranges = [] 351 for ref in references: 352 ranges.extend(ref.ranges) 353 new_ref = reference(*combine_ranges(ranges)) 354 return new_ref
For a list of references, combine their ranges into a new reference.
Combining means sorting ranges, merging overlaps, and joining adjacent records.
360 def collate( 361 self, references: list[Reference] 362 ) -> list[tuple[Library, list[tuple[Book, list[Reference]]]]]: 363 """ 364 The default collation is by book; use collate_chapter_references 365 otherwise. 366 """ 367 return self.collate_book_references(references)
The default collation is by book; use collate_chapter_references otherwise.
369 def collate_book_references( 370 self, references: list[Reference] 371 ) -> list[tuple[Library, list[tuple[Book, list[Reference]]]]]: 372 """ 373 Collate references into nested library and book dictionaries for easy 374 looping. 375 376 Example: 377 ``` 378 for library, book_collation in __.collate_books_references(refs): 379 for book, references in book_collation: 380 # ... 381 ``` 382 """ 383 library_list = list() 384 for library_id, books_dict in self.collate_by_book(references).items(): 385 book_list = list() 386 for book_id, references in books_dict.items(): 387 book_list.append(tuple([self.books[library_id, book_id], references])) 388 library_list.append(tuple([self.libraries[library_id], book_list])) 389 return library_list
Collate references into nested library and book dictionaries for easy looping.
Example:
for library, book_collation in __.collate_books_references(refs): for book, references in book_collation: # ...
391 def collate_by_book( 392 self, references: list[Reference] 393 ) -> dict[Number, dict[Number, list[Reference]]]: 394 """ 395 A collation groups single-book references by library and book IDs. 396 Multi-book references are ignored. 397 """ 398 collation = dict() 399 for ref in [_ for _ in references if _.count_books() == 1]: 400 v1 = ref.ranges[0].start 401 if v1.library not in collation: 402 collation[v1.library] = dict() 403 if v1.book not in collation[v1.library]: 404 collation[v1.library][v1.book] = list() 405 collation[v1.library][v1.book].append(ref) 406 return collation
A collation groups single-book references by library and book IDs. Multi-book references are ignored.
408 def collate_chapter_references( 409 self, references: list[Reference] 410 ) -> list[tuple[Library, list[tuple[Book, list[tuple[Number, list[Reference]]]]]]]: 411 """ 412 Collate references into nested library, book, and chapter dictionaries 413 for easy looping. 414 415 Example: 416 ``` 417 for library, book_collation in __.collate_chapter_references(refs): 418 for book, chapter_collation in book_collation: 419 for chapter_number, references in chapter_collation: 420 # ... 421 ``` 422 """ 423 library_list = list() 424 for library_id, books_dict in self.collate_by_chapter(references).items(): 425 book_list = list() 426 for book_id, chapters_dict in books_dict.items(): 427 chapter_list = list() 428 for chapter_id, references in chapters_dict.items(): 429 chapter_list.append(tuple([chapter_id, references])) 430 book_list.append(tuple([self.books[library_id, book_id], chapter_list])) 431 library_list.append(tuple([self.libraries[library_id], book_list])) 432 return library_list
Collate references into nested library, book, and chapter dictionaries for easy looping.
Example:
for library, book_collation in __.collate_chapter_references(refs): for book, chapter_collation in book_collation: for chapter_number, references in chapter_collation: # ...
434 def collate_by_chapter( 435 self, references: list[Reference] 436 ) -> dict[Number, dict[Number, dict[Number, list[Reference]]]]: 437 """ 438 A collation groups single-book references by library, book, and chapter 439 IDs. Multi-book references are ignored. 440 """ 441 collation = dict() 442 for ref in [_ for _ in references if _.count_books() == 1]: 443 v1 = ref.ranges[0].start 444 if v1.library not in collation: 445 collation[v1.library] = dict() 446 if v1.book not in collation[v1.library]: 447 collation[v1.library][v1.book] = dict() 448 if v1.chapter not in collation[v1.library][v1.book]: 449 collation[v1.library][v1.book][v1.chapter] = list() 450 collation[v1.library][v1.book][v1.chapter].append(ref) 451 return collation
A collation groups single-book references by library, book, and chapter IDs. Multi-book references are ignored.
453 def collate_verse_references( 454 self, references: list[Reference] 455 ) -> list[ 456 tuple[ 457 Library, list[tuple[Book, list[tuple[Number, list[tuple[str, Reference]]]]]] 458 ] 459 ]: 460 """ 461 Collate references into nested library, book, chapter and verse-range 462 dictionaries for easy looping. 463 464 Example: 465 ``` 466 for library, book_collation in __.collate_verse_references(refs): 467 for book, chapter_collation in book_collation: 468 for chapter_number, verse_collation in chapter_collation: 469 for verse_numbers, references in verse_collation: 470 # ... 471 ``` 472 473 Note: 474 - verse_numbers is a unique string representing the verse ranges, 475 e.g. '1, 5-6, 13' 476 """ 477 library_list = list() 478 for library_id, books_dict in self.collate_by_verse(references).items(): 479 book_list = list() 480 for book_id, chapters_dict in books_dict.items(): 481 chapter_list = list() 482 for chapter_id, verses_dict in chapters_dict.items(): 483 verse_list = list() 484 for ref_numbers, references in verses_dict.items(): 485 verse_list.append(tuple([ref_numbers, references])) 486 chapter_list.append(tuple([chapter_id, verse_list])) 487 book_list.append(tuple([self.books[library_id, book_id], chapter_list])) 488 library_list.append(tuple([self.libraries[library_id], book_list])) 489 return library_list
Collate references into nested library, book, chapter and verse-range dictionaries for easy looping.
Example:
for library, book_collation in __.collate_verse_references(refs): for book, chapter_collation in book_collation: for chapter_number, verse_collation in chapter_collation: for verse_numbers, references in verse_collation: # ...
Note:
- verse_numbers is a unique string representing the verse ranges, e.g. '1, 5-6, 13'
491 def collate_by_verse( 492 self, references: list[Reference], aggregate_function=None 493 ) -> dict[Number, dict[Number, dict[Number, dict[str, list[Reference]]]]]: 494 """ 495 A collation groups single-book references by library, book, chapter 496 number, and a string reresenting the verse ranges. Multi-book 497 references are ignored. 498 """ 499 collation = dict() 500 for ref in [_ for _ in references if _.count_books() == 1]: 501 v1 = ref.ranges[0].start 502 if v1.library not in collation: 503 collation[v1.library] = dict() 504 if v1.book not in collation[v1.library]: 505 collation[v1.library][v1.book] = dict() 506 if v1.chapter not in collation[v1.library][v1.book]: 507 collation[v1.library][v1.book][v1.chapter] = dict() 508 ref_numbers = self.numbers(ref) 509 if ref_numbers not in collation[v1.library][v1.book][v1.chapter]: 510 collation[v1.library][v1.book][v1.chapter][ref_numbers] = list() 511 collation[v1.library][v1.book][v1.chapter][ref_numbers].append(ref) 512 return collation
A collation groups single-book references by library, book, chapter number, and a string reresenting the verse ranges. Multi-book references are ignored.
518 def first_reference(self, text: str) -> tuple[str | None, Reference | None]: 519 """ 520 Return the first tuple of (match_str, reference) found by 521 `refspy.manager.Manager.generate_references()` 522 """ 523 generator = self.matcher.generate_references(text) 524 return next(generator, (None, None))
Return the first tuple of (match_str, reference) found by
refspy.manager.Manager.generate_references()
526 def find_references( 527 self, 528 text: str, 529 include_books: bool = False, 530 include_nones: bool = False, 531 use_context: bool = True, 532 ) -> list[tuple[str, Reference | None]]: 533 """ 534 Return a list of tuples of (match_str, reference) found by 535 `refspy.manager.Manager.generate_references()` 536 """ 537 generator = self.matcher.generate_references( 538 text, include_books, include_nones, use_context 539 ) 540 return list(generator)
Return a list of tuples of (match_str, reference) found by
refspy.manager.Manager.generate_references()
542 def generate_references( 543 self, 544 text: str, 545 yield_books: bool = False, 546 yield_nones: bool = False, 547 use_context: bool = True, 548 ) -> Generator[tuple[str, Reference | None], None, None]: 549 """ 550 Generate tuples of (match_str, reference) for provided text.manager 551 552 Task delegated to `refspy.matcher.Matcher.generate_references()`. 553 554 Args: 555 text: a string to search for references 556 yield_books: Whether to yield book names without reference numbers 557 yield_nones: Whether to yield (match_str, None) for malformed 558 references. 559 use_context: Whether to yield anything other than exact book and 560 number matches 561 562 Yield: 563 A tuple of `(match_str, reference)` for each valid reference. 564 """ 565 yield from self.matcher.generate_references( 566 text, yield_books, yield_nones, use_context 567 )
Generate tuples of (match_str, reference) for provided text.manager
Task delegated to refspy.matcher.Matcher.generate_references().
Arguments:
- text: a string to search for references
- yield_books: Whether to yield book names without reference numbers
- yield_nones: Whether to yield (match_str, None) for malformed references.
- use_context: Whether to yield anything other than exact book and number matches
Yield:
A tuple of
(match_str, reference)for each valid reference.
573 def bcv( 574 self, 575 alias: str, 576 c: Number | None = None, 577 v: Number | None = None, 578 v_end: Number | None = None, 579 ) -> Reference: 580 """Construct a reference from a book alias and chapter/verse numbers. 581 582 Omitting optional arguments will construct a reference to a whole book 583 or chapter. 584 585 Args: 586 v_end: constructs a verse range from `v` to `v_end`. 587 588 Raises: 589 ValueError: If the book_alias is missing, or the number values are 590 out of range. 591 """ 592 if alias not in self.book_aliases: 593 raise ValueError(f'Book alias "{alias}" not found.') 594 library_id, book_id = self.book_aliases[alias] 595 ta = TypeAdapter(Number) 596 if c is None: 597 return book_reference(library_id, book_id) 598 if v is None: 599 return chapter_reference(library_id, book_id, ta.validate_python(c)) 600 601 if not v_end: 602 return verse_reference( 603 library_id, 604 book_id, 605 ta.validate_python(c), 606 ta.validate_python(v), 607 ) 608 else: 609 return reference( 610 range( 611 verse( 612 library_id, 613 book_id, 614 ta.validate_python(c), 615 ta.validate_python(v), 616 ), 617 verse( 618 library_id, 619 book_id, 620 ta.validate_python(c), 621 ta.validate_python(v_end), 622 ), 623 ) 624 )
Construct a reference from a book alias and chapter/verse numbers.
Omitting optional arguments will construct a reference to a whole book or chapter.
Arguments:
- v_end: constructs a verse range from
vtov_end.
Raises:
- ValueError: If the book_alias is missing, or the number values are out of range.
626 def bcr( 627 self, alias: str, c: Number, v_ranges: list[Number | tuple[Number, Number]] 628 ) -> Reference: 629 if alias not in self.book_aliases: 630 raise ValueError(f'Book alias "{alias}" not found.') 631 library_id, book_id = self.book_aliases[alias] 632 ranges = [] 633 for val in v_ranges: 634 if isinstance(val, tuple): 635 v_start, v_end = val 636 ranges.append( 637 range( 638 verse(library_id, book_id, c, v_start), 639 verse(library_id, book_id, c, v_end), 640 ) 641 ) 642 elif isinstance(val, int): 643 v_start = v_end = val 644 ranges.append( 645 range( 646 verse(library_id, book_id, c, v_start), 647 verse(library_id, book_id, c, v_end), 648 ) 649 ) 650 return reference(*ranges)
652 def r(self, text: str) -> Reference | None: 653 """Return the first matching reference in the given text. 654 655 Book names and malformed references are not matched. 656 """ 657 _, reference = self.first_reference(text) 658 return reference
Return the first matching reference in the given text.
Book names and malformed references are not matched.
664 def next_chapter(self, ref: Reference) -> Reference | None: 665 """Get the next chapter to the one containing this reference. 666 667 This loops over the end of books using `refspy.models.book.Book.chapters`. 668 This does not loop beyond the end of libraries. 669 """ 670 return self.navigator.next_chapter(ref)
Get the next chapter to the one containing this reference.
This loops over the end of books using refspy.models.book.Book.chapters.
This does not loop beyond the end of libraries.
672 def prev_chapter(self, ref: Reference) -> Reference | None: 673 """Get the previous chapter to the one containing this reference. 674 675 This loops over the end of books using `refspy.models.book.Book.chapters`, but 676 not beyond the end of libraries. 677 """ 678 return self.navigator.prev_chapter(ref)
Get the previous chapter to the one containing this reference.
This loops over the end of books using refspy.models.book.Book.chapters, but
not beyond the end of libraries.
684 def get_book(self, ref: Reference) -> Book: 685 """Get the book object for this reference's first range.""" 686 v1 = ref.ranges[0].start 687 return self.books[v1.library, v1.book]
Get the book object for this reference's first range.
689 def book_reference(self, ref: Reference) -> Reference: 690 """Create a reference to the book containing this reference's first 691 range.""" 692 v1 = ref.ranges[0].start 693 return reference( 694 range( 695 verse(v1.library, v1.book, 1, 1), 696 verse(v1.library, v1.book, 999, 999), 697 ) 698 )
Create a reference to the book containing this reference's first range.
700 def chapter_reference(self, ref: Reference) -> Reference: 701 """Create a reference to the chapter containing this reference's first 702 range.""" 703 v1 = ref.ranges[0].start 704 return reference( 705 range( 706 verse(v1.library, v1.book, v1.chapter, 1), 707 verse(v1.library, v1.book, v1.chapter, 999), 708 ) 709 )
Create a reference to the chapter containing this reference's first range.
715 def link(self, ref: Reference) -> str: 716 """Format a URL Link, with English style number references""" 717 return self.formatter.format(ref, self.formatter.link_format())
Format a URL Link, with English style number references
719 def name(self, ref: Reference) -> str: 720 """Format a reference.""" 721 return self.formatter.format(ref, self.formatter.name_format(self.language))
Format a reference.
723 def book(self, ref: Reference) -> str: 724 """Format a reference using only the book part of its name.""" 725 return self.formatter.format(ref, self.formatter.book_format(self.language))
Format a reference using only the book part of its name.
727 def abbrev_name(self, ref: Reference) -> str: 728 """Format an abbreviated reference.""" 729 return self.formatter.format( 730 ref, self.formatter.abbrev_name_format(self.language) 731 )
Format an abbreviated reference.
733 def abbrev_book(self, ref: Reference) -> str: 734 """Format an abbreviated reference using only the book part of its name.""" 735 return self.formatter.format( 736 ref, self.formatter.abbrev_book_format(self.language) 737 )
Format an abbreviated reference using only the book part of its name.
739 def numbers(self, ref: Reference) -> str: 740 """Format a reference using only the number part of its name.""" 741 return self.formatter.format(ref, self.formatter.number_format(self.language))
Format a reference using only the number part of its name.
743 def abbrev_numbers(self, ref: Reference) -> str: 744 """Format an abbreviated reference using only the number part of its name.""" 745 return self.formatter.format( 746 ref, self.formatter.abbrev_number_format(self.language) 747 )
Format an abbreviated reference using only the number part of its name.
753 def template(self, reference: Reference | None, pattern: str | None = None) -> str: 754 """ 755 Substitute formatting values in a string: 756 757 * `{LINK}` -> "1%20Cor%202:3-4" 758 - like ESC_ABBREV_NAME, but with English numbering in any language. 759 - useful for BibleGateway and presumably other sites. 760 * `{NAME}` -> "1 Corinthians 2:3–4" 761 * `{BOOK}` -> "1 Corinthians" 762 * `{NUMBERS}` -> "2:3–4" 763 * `{ABBREV_NAME}` -> "1 Cor 2:3–4" 764 * `{ABBREV_BOOK}` -> "1 Cor" 765 * `{ABBREV_NUMBERS}` -> "2:3–4" 766 * `{ESC_NAME}` -> "1%20Corinthians%202%3A3-4" 767 * `{ESC_BOOK}` -> "1%20Corinthians" 768 * `{ESC_NUMBERS}` -> "2%3A3-4" 769 * `{ESC_ABBREV_NAME}` -> "1%20Cor%202%3A3-4" 770 * `{ESC_ABBREV_BOOK}` -> "1%20Cor" 771 * `{ESC_ABBREV_NUMBERS}` -> "2%3A3-4" 772 * `{PARAM_NAME}`-> "1cor+2.3-4" 773 * `{PARAM_BOOK}` -> "1cor" 774 * `{PARAM_NUMBERS}` -> "2.3-4" 775 776 For efficiency, we calculate only the values required by the template 777 string. 778 """ 779 if reference is None: 780 return "" 781 782 out = pattern if pattern else "{ABBREV_NAME}" 783 784 regexp = re.compile(r"\{[A-Z_]+\}") 785 matches = regexp.findall(out) 786 numbers = self.numbers(reference) 787 for _ in matches: 788 if _ == "{LINK}": 789 out = out.replace("{LINK}", url_escape(self.link(reference))) 790 elif _ == "{NAME}": 791 out = out.replace("{NAME}", self.name(reference)) 792 elif _ == "{BOOK}": 793 out = out.replace("{BOOK}", self.book(reference)) 794 elif _ == "{NUMBERS}": 795 out = out.replace("{NUMBERS}", numbers) 796 elif _ == "{ASCII_NUMBERS}": 797 out = out.replace("{ASCII_NUMBERS}", numbers.replace("–", "-")) 798 elif _ == "{ABBREV_NAME}": 799 out = out.replace("{ABBREV_NAME}", self.abbrev_name(reference)) 800 elif _ == "{ABBREV_BOOK}": 801 out = out.replace("{ABBREV_BOOK}", self.abbrev_name(reference)) 802 elif _ == "{ESC_NAME}": 803 out = out.replace("{ESC_NAME}", url_escape(self.name(reference))) 804 elif _ == "{ESC_BOOK}": 805 out = out.replace("{ESC_BOOK}", url_escape(self.book(reference))) 806 elif _ == "{ESC_NUMBERS}": 807 out = out.replace("{ESC_NUMBERS}", url_escape(numbers)) 808 elif _ == "{ESC_ASCII_NUMBERS}": 809 out = out.replace( 810 "{ESC_ASCII_NUMBERS}", 811 url_escape(numbers.replace("–", "-")), 812 ) 813 elif _ == "{ESC_ABBREV_NAME}": 814 out = out.replace( 815 "{ESC_ABBREV_NAME}", url_escape(self.abbrev_name(reference)) 816 ) 817 elif _ == "{ESC_ABBREV_BOOK}": 818 out = out.replace( 819 "{ESC_ABBREV_BOOK}", 820 url_escape(self.abbrev_book(reference)), 821 ) 822 elif _ == "{PARAM_NAME}": 823 out = out.replace("{PARAM_NAME}", url_param(self.name(reference))) 824 elif _ == "{PARAM_BOOK}": 825 out = out.replace("{PARAM_BOOK}", url_param(numbers)) 826 elif _ == "{PARAM_NUMBERS}": 827 out = out.replace("{PARAM_NUMBERS}", url_param(numbers)) 828 return out
Substitute formatting values in a string:
{LINK}-> "1%20Cor%202:3-4"
- like ESC_ABBREV_NAME, but with English numbering in any language.
- useful for BibleGateway and presumably other sites.
{NAME}-> "1 Corinthians 2:3–4"{BOOK}-> "1 Corinthians"{NUMBERS}-> "2:3–4"{ABBREV_NAME}-> "1 Cor 2:3–4"{ABBREV_BOOK}-> "1 Cor"{ABBREV_NUMBERS}-> "2:3–4"{ESC_NAME}-> "1%20Corinthians%202%3A3-4"{ESC_BOOK}-> "1%20Corinthians"{ESC_NUMBERS}-> "2%3A3-4"{ESC_ABBREV_NAME}-> "1%20Cor%202%3A3-4"{ESC_ABBREV_BOOK}-> "1%20Cor"{ESC_ABBREV_NUMBERS}-> "2%3A3-4"{PARAM_NAME}-> "1cor+2.3-4"{PARAM_BOOK}-> "1cor"{PARAM_NUMBERS}-> "2.3-4"
For efficiency, we calculate only the values required by the template string.