capita 1.25.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. capita-1.25.1/PKG-INFO +28 -0
  2. capita-1.25.1/README +1 -0
  3. capita-1.25.1/capita/__init__.py +79 -0
  4. capita-1.25.1/capita/__main__.py +13 -0
  5. capita-1.25.1/capita/__patch__.py +70 -0
  6. capita-1.25.1/capita/biblio/__init__.py +8 -0
  7. capita-1.25.1/capita/biblio/utils.py +76 -0
  8. capita-1.25.1/capita/chapter/__init__.py +8 -0
  9. capita-1.25.1/capita/chapter/outlines.py +129 -0
  10. capita-1.25.1/capita/chapter/run.py +123 -0
  11. capita-1.25.1/capita/chapter/starter.py +174 -0
  12. capita-1.25.1/capita/chapter/utils.py +45 -0
  13. capita-1.25.1/capita/cli.py +204 -0
  14. capita-1.25.1/capita/creator.py +95 -0
  15. capita-1.25.1/capita/feature/__init__.py +153 -0
  16. capita-1.25.1/capita/feature/abbreviation.py +195 -0
  17. capita-1.25.1/capita/feature/abstract.py +88 -0
  18. capita-1.25.1/capita/feature/acknowledge.py +44 -0
  19. capita-1.25.1/capita/feature/appendix.py +79 -0
  20. capita-1.25.1/capita/feature/chapter.py +72 -0
  21. capita-1.25.1/capita/feature/docinfo.py +57 -0
  22. capita-1.25.1/capita/feature/figuretable.py +79 -0
  23. capita-1.25.1/capita/feature/glossary.py +157 -0
  24. capita-1.25.1/capita/feature/index.py +71 -0
  25. capita-1.25.1/capita/feature/legal.py +149 -0
  26. capita-1.25.1/capita/feature/paper.py +57 -0
  27. capita-1.25.1/capita/feature/section.py +47 -0
  28. capita-1.25.1/capita/feature/symboltable.py +46 -0
  29. capita-1.25.1/capita/feature/tabletable.py +77 -0
  30. capita-1.25.1/capita/feature/title.py +159 -0
  31. capita-1.25.1/capita/feature/toc.py +101 -0
  32. capita-1.25.1/capita/feature/whitepage.py +168 -0
  33. capita-1.25.1/capita/paper/__init__.py +8 -0
  34. capita-1.25.1/capita/paper/fonts.py +32 -0
  35. capita-1.25.1/capita/paper/main.py +157 -0
  36. capita-1.25.1/capita/path.py +160 -0
  37. capita-1.25.1/capita/section/__init__.py +8 -0
  38. capita-1.25.1/capita/section/after.py +124 -0
  39. capita-1.25.1/capita/section/ctor.py +38 -0
  40. capita-1.25.1/capita/section/improve.py +151 -0
  41. capita-1.25.1/capita/section/load.py +120 -0
  42. capita-1.25.1/capita/section/run.py +141 -0
  43. capita-1.25.1/capita/strategy.py +177 -0
  44. capita-1.25.1/capita/table.py +28 -0
  45. capita-1.25.1/capita/utils/__init__.py +17 -0
  46. capita-1.25.1/capita/utils/headline.py +158 -0
  47. capita-1.25.1/capita/utils/spa.py +77 -0
  48. capita-1.25.1/capita/utils/text.py +112 -0
  49. capita-1.25.1/capita.egg-info/PKG-INFO +28 -0
  50. capita-1.25.1/capita.egg-info/SOURCES.txt +72 -0
  51. capita-1.25.1/capita.egg-info/dependency_links.txt +1 -0
  52. capita-1.25.1/capita.egg-info/entry_points.txt +2 -0
  53. capita-1.25.1/capita.egg-info/requires.txt +15 -0
  54. capita-1.25.1/capita.egg-info/top_level.txt +1 -0
  55. capita-1.25.1/pyproject.toml +110 -0
  56. capita-1.25.1/setup.cfg +4 -0
  57. capita-1.25.1/tests/test_abbreviation.py +109 -0
  58. capita-1.25.1/tests/test_abstract.py +68 -0
  59. capita-1.25.1/tests/test_appendix.py +50 -0
  60. capita-1.25.1/tests/test_chapter.py +179 -0
  61. capita-1.25.1/tests/test_cli.py +45 -0
  62. capita-1.25.1/tests/test_docinfo.py +26 -0
  63. capita-1.25.1/tests/test_glossary.py +55 -0
  64. capita-1.25.1/tests/test_headlines.py +34 -0
  65. capita-1.25.1/tests/test_index.py +74 -0
  66. capita-1.25.1/tests/test_legal.py +47 -0
  67. capita-1.25.1/tests/test_old.py +111 -0
  68. capita-1.25.1/tests/test_table.py +30 -0
  69. capita-1.25.1/tests/test_textonpage.py +52 -0
  70. capita-1.25.1/tests/test_textprocessor.py +46 -0
  71. capita-1.25.1/tests/test_title.py +103 -0
  72. capita-1.25.1/tests/test_toc.py +127 -0
  73. capita-1.25.1/tests/test_validate.py +80 -0
  74. capita-1.25.1/tests/test_whitepage.py +115 -0
capita-1.25.1/PKG-INFO ADDED
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: capita
3
+ Version: 1.25.1
4
+ Author-email: Helmut Konrad Schewe <helmutus@outlook.com>
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/anaticulae/capita
7
+ Project-URL: Repository, https://github.com/anaticulae/capita
8
+ Classifier: Programming Language :: Python :: 3.12
9
+ Classifier: Programming Language :: Python :: 3.13
10
+ Classifier: Programming Language :: Python :: 3.14
11
+ Requires-Python: >=3.12
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: configos<2.0.0,>=1.0.4
14
+ Requires-Dist: elementae<2.0.0,>=1.0.1
15
+ Requires-Dist: geostrat<2.0.0,>=1.5.1
16
+ Requires-Dist: germania<2.0.0,>=1.32.0
17
+ Requires-Dist: iamraw<5.0.0,>=4.91.6
18
+ Requires-Dist: layouta<2.0.0,>=1.0.0
19
+ Requires-Dist: utilo<3.0.0,>=2.109.0
20
+ Provides-Extra: dev
21
+ Requires-Dist: gennex==1.0.3; extra == "dev"
22
+ Requires-Dist: groupmes<2.0.0,>=1.1.0; extra == "dev"
23
+ Requires-Dist: hoverpower==1.4.3; extra == "dev"
24
+ Requires-Dist: pagenumber==1.0.0; extra == "dev"
25
+ Requires-Dist: rawmaker==2.40.3; extra == "dev"
26
+ Requires-Dist: utilotest<2.0.0,>=1.0.4; extra == "dev"
27
+
28
+ # sections
capita-1.25.1/README ADDED
@@ -0,0 +1 @@
1
+ # sections
@@ -0,0 +1,79 @@
1
+ #==============================================================================
2
+ # C O P Y R I G H T
3
+ #------------------------------------------------------------------------------
4
+ # Copyright (c) 2019-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ #==============================================================================
9
+ """Sections
10
+ ========
11
+
12
+ The `sections` tool is a very lightwight tool to determine the
13
+ likelihood of a feature on a page very quickly. Sections is runned to
14
+ determine which feature extraction should runned on which pages.
15
+
16
+ Plan
17
+ ----
18
+
19
+ The result of the section tool is a plan for the feature extractor.
20
+ Further plans for judgements and informations for the user are not
21
+ provided by sections tool. The section tool extracts only features but
22
+ do not judges the result.
23
+
24
+ Sources
25
+ -------
26
+
27
+ Data Provider:
28
+
29
+ * Rawmaker
30
+ * Linero
31
+ * Imagero (planned)
32
+ * Figero (planned)
33
+ * Tabelero (planned)
34
+
35
+
36
+ Feature Provider:
37
+
38
+ * Groupme
39
+ * Detector
40
+
41
+ Planned Features
42
+ ----------------
43
+
44
+ The following sections are planned to be supported:
45
+
46
+ .. code-block:: none
47
+
48
+ * Introduction
49
+ * Titlepage
50
+ * Thank you
51
+ * Copyright etc.
52
+ * Erklaerung
53
+ * Table-Area
54
+ * Table of content
55
+ * Short cuts
56
+ * Figure table
57
+ * Content
58
+ * Chapter
59
+ * Figure
60
+ * Text
61
+ * Headlines
62
+ * Table-Area-B
63
+ * Appendix
64
+ * Resources
65
+ * Link
66
+ * Bibliography
67
+ """
68
+
69
+ import importlib.metadata
70
+ import os
71
+
72
+ import capita.__patch__
73
+ import capita.path
74
+
75
+ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
76
+
77
+ PACKAGE = 'capita'
78
+ PROCESS = 'sections'
79
+ __version__ = importlib.metadata.version(PACKAGE)
@@ -0,0 +1,13 @@
1
+ #==============================================================================
2
+ # C O P Y R I G H T
3
+ #------------------------------------------------------------------------------
4
+ # Copyright (c) 2020-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ #==============================================================================
9
+
10
+ from capita.cli import main
11
+
12
+ if __name__ == "__main__":
13
+ main()
@@ -0,0 +1,70 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2020-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import re
11
+
12
+ import germania
13
+ import utilo
14
+
15
+
16
+ def authors(raw: str, verbose: bool = False) -> list:
17
+ """\
18
+ >>> authors('PEREIRA, M.G., VOLCHAN, E., SOUZA, G. G. DE, OLIVEIRA, L.,'
19
+ ... 'CAMPAGNOLI, R. R., PINHEIRO, W. M., & PESSOA, L. ')
20
+ ['PEREIRA, M.G.', 'VOLCHAN, E.', 'SOUZA, G. G. DE', 'OLIVEIRA, L.', 'CAMPAGNOLI, R. R.', 'PINHEIRO, W. M.', 'PESSOA, L.']
21
+ """
22
+ pattern = r"""
23
+ [a-zA-Z]{4,}\,
24
+ [ ]{0,2}
25
+ ([a-zA-Z]\.[ ]{0,2}){1,3}
26
+ [ ]{0,2}
27
+ (DE)?
28
+ """
29
+ result = []
30
+ for item in re.finditer(pattern, raw, re.VERBOSE):
31
+ item = utilo.extract_match(item).strip()
32
+ if verbose:
33
+ item = (item, item)
34
+ result.append(item)
35
+ return result
36
+
37
+
38
+ germania.authors = authors
39
+
40
+
41
+ def a_minus_b(a: set | list, b: set | list) -> set | list:
42
+ result = []
43
+ for item in a:
44
+ if item in b:
45
+ continue
46
+ result.append(item)
47
+ if isinstance(a, set):
48
+ result: set = set(result)
49
+ return result
50
+
51
+
52
+ utilo.a_minus_b = a_minus_b
53
+
54
+
55
+ def union(*items: set | list):
56
+ """\
57
+ >>> union([1, 2, 3])
58
+ [1, 2, 3]
59
+ >>> union([1, 2, 3], [2, 2, 2])
60
+ [1, 2, 3]
61
+ """
62
+ result = []
63
+ for item in items:
64
+ for it in item:
65
+ result.append(it)
66
+ result = utilo.make_unique(result)
67
+ return result
68
+
69
+
70
+ utilo.union = union
@@ -0,0 +1,8 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2020-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
@@ -0,0 +1,76 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2020-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import configos
11
+ import iamraw
12
+ import utilo
13
+
14
+ DIFF_MAX = configos.HolyTable(
15
+ items=(
16
+ (1, 0),
17
+ (5, 0),
18
+ (6, 1),
19
+ (10, 2),
20
+ (12, 2),
21
+ ),
22
+ right_outranges_none=False,
23
+ )
24
+
25
+
26
+ def cluster_bibpages(
27
+ items,
28
+ likelihood_name='bibliography_table',
29
+ ):
30
+ """Select hugest(max sum likelihood value) group."""
31
+ if not items:
32
+ return []
33
+ maxdiff = DIFF_MAX(len(items)) + 1
34
+ grouped = utilo.groupby_diff(
35
+ items,
36
+ maxdiff=maxdiff,
37
+ selector=lambda x: x.page,
38
+ )
39
+ if not grouped:
40
+ return []
41
+ hugest = sorted(
42
+ grouped,
43
+ key=lambda x: sum(item.content.value for item in x),
44
+ )
45
+ hugest = hugest[-1]
46
+ avg = sum((item.content.value for item in hugest)) / len(hugest)
47
+ avg = utilo.roundme(avg)
48
+ for item in hugest:
49
+ # every item of the group should have the same likelihood
50
+ item.content.value = avg
51
+ result = fill_empty(
52
+ hugest,
53
+ likelihood_name=likelihood_name,
54
+ )
55
+ return result
56
+
57
+
58
+ def fill_empty(items: list, likelihood_name: str) -> list:
59
+ """Fill holes inside connected bib."""
60
+ result = list(items)
61
+ start, end = result[0].page, result[-1].page
62
+ done = {item.page for item in result}
63
+ avg = result[0].content.value
64
+ for page in utilo.rlist(start, end):
65
+ if page in done:
66
+ continue
67
+ result.append(
68
+ iamraw.PageContentLikelihood(
69
+ page=page,
70
+ content=iamraw.Likelihood(
71
+ value=avg,
72
+ name=likelihood_name,
73
+ ),
74
+ ))
75
+ result.sort(key=lambda x: x.page)
76
+ return result
@@ -0,0 +1,8 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2022-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
@@ -0,0 +1,129 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2022-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+ """Chapter: Outlines
10
+ =================
11
+
12
+ Try to find text on the page which matches with lines from the outlines.
13
+
14
+ In most cases, if outlines are given, there represent the headlines
15
+ inside the document.
16
+ """
17
+
18
+ import re
19
+
20
+ import configos
21
+ import elementae.headline.lookup
22
+ import utilo
23
+
24
+ import capita.chapter.utils
25
+
26
+ TOCS_COUNT_MIN = configos.HV_INT_PLUS(default=3)
27
+
28
+
29
+ def rate_from_outlines(tocs, pagestart: list) -> float:
30
+ """Try to find potential headline inside toc from outlines.
31
+
32
+ If no toc is given, use elementae.headlines-list as backup strategy.
33
+ """
34
+ chapter_rate = 0.0
35
+ if len(tocs) >= TOCS_COUNT_MIN:
36
+ chapter_rate = contains_outline(pagestart, tocs)
37
+ return chapter_rate
38
+ # disable feature if no toc is given
39
+ utilo.info('chapter: no toc provided')
40
+ # try backup
41
+ matched = any(
42
+ utilo.similar(
43
+ expected=HEADLINES_BACKUP,
44
+ current=item.text,
45
+ maxdiff=0.9,
46
+ ) for item in pagestart)
47
+ if matched:
48
+ return 1.0
49
+ return 0.0
50
+
51
+
52
+ HEADLINES_BACKUP = elementae.headline.lookup.CHAPTER
53
+
54
+
55
+ def contains_outline(content, toc) -> float:
56
+ """Check that content starts with a parsed headline entree.
57
+
58
+ Supported Pattern; line starts with:
59
+ * 3. Headline text
60
+ * Headline text
61
+ """
62
+ flat_toc = level_remove(toc)
63
+ flat_toc = toc_shrink(flat_toc)
64
+ if not flat_toc:
65
+ # no table of content was extracted
66
+ return 0.0
67
+ flat_toc = [item for item in flat_toc if item.lower() not in NOHEADLINES]
68
+ for line in content:
69
+ line = utilo.normalize_whitespaces(line.text.strip())
70
+ if utilo.issinglechar(line):
71
+ # I N T R O D U C T I O N 1
72
+ line = line.replace(' ', '')
73
+ # remove numbered headline pattern and potential white spaces
74
+ without_number = FIRSTLEVEL_DOT_PATTERN.sub('', line)
75
+ for headline in flat_toc:
76
+ if all((
77
+ not capita.chapter.utils.startswith(line, headline),
78
+ not capita.chapter.utils.startswith(without_number,
79
+ headline),
80
+ )):
81
+ continue
82
+ rate = len(without_number) / len(headline)
83
+ if not 0.2 < rate < 2.0:
84
+ # headline is too long or too short, could not match with
85
+ # detected toc
86
+ continue
87
+ return 1.0
88
+ return -0.5
89
+
90
+
91
+ NOHEADLINES = utilo.a_minus_b( #pylint:disable=no-member
92
+ elementae.headline.lookup.HEADLINES,
93
+ elementae.headline.lookup.CHAPTER,
94
+ )
95
+
96
+ FIRSTLEVEL_DOT_PATTERN = re.compile(r'^\d{1,2}\.{0,1}\s+')
97
+
98
+
99
+ def level_remove(toc):
100
+ flat = []
101
+ for item in toc:
102
+ if not item.title.strip():
103
+ # invalid outlines can break further processing
104
+ utilo.debug('empty outline element')
105
+ continue
106
+ flat.append(FIRSTLEVEL_DOT_PATTERN.sub('', item.title))
107
+ # remove roman level
108
+ result = []
109
+ for item in flat:
110
+ splitted = item.split(maxsplit=1)
111
+ if len(splitted) == 1:
112
+ result.append(item)
113
+ continue
114
+ if utilo.isroman(splitted[0]):
115
+ result.append(splitted[1])
116
+ continue
117
+ result.append(item)
118
+ return result
119
+
120
+
121
+ def toc_shrink(items):
122
+ """Remove tocs after Anhang."""
123
+ result = []
124
+ for item in items:
125
+ if 'anhang' in item.lower():
126
+ # TODO: DANGEROUS! ANHANG CAN BE PART OF ANY OTHER HEADLINE
127
+ break
128
+ result.append(item)
129
+ return result
@@ -0,0 +1,123 @@
1
+ # =============================================================================
2
+ # C O P Y R I G H T
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2022-2023 by Helmut Konrad Schewe. All rights reserved.
5
+ # This file is property of Helmut Konrad Schewe. Any unauthorized copy,
6
+ # use or distribution is an offensive act against international law and may
7
+ # be prosecuted under federal law. Its content is company confidential.
8
+ # =============================================================================
9
+
10
+ import statistics
11
+
12
+ import configos
13
+ import iamraw
14
+ import texmex
15
+ import utilo
16
+
17
+ import capita.chapter.outlines
18
+ import capita.chapter.starter
19
+
20
+ FIRST_QUARTER = configos.HV_PERCENT_PLUS(default=45.0)
21
+
22
+
23
+ def extract_chapter(
24
+ navigators: texmex.PTNs,
25
+ outlines,
26
+ ) -> iamraw.PageContentLikelihoods:
27
+ result = []
28
+ for page in navigators:
29
+ if nochapter(page):
30
+ continue
31
+ # prepare content
32
+ pagestart = page.before(FIRST_QUARTER)
33
+ # run strategies
34
+ rate = capita.chapter.starter.contain_chapter(pagestart)
35
+ rate += capita.chapter.outlines.rate_from_outlines(
36
+ outlines,
37
+ pagestart,
38
+ )
39
+ if rate <= 0.0:
40
+ # TODO: VERY VERBOSE, CHECK LATER
41
+ utilo.verbose(f' no chapter {page.page}: chapterate {rate}')
42
+ continue
43
+ # convert result to percents
44
+ rate_in_percent = chaptervalue_to_percent(rate, outlines)
45
+ result.append(
46
+ iamraw.PageContentLikelihood(
47
+ page=page.page,
48
+ content=iamraw.Likelihood(rate_in_percent, 'chapter'),
49
+ ))
50
+ # TODO: There is the possiblity that header and start of chapter
51
+ # are together, support later
52
+ # result.append(0.0)
53
+ return result
54
+
55
+
56
+ def nochapter(page) -> bool:
57
+ if not page:
58
+ # empty page
59
+ return True
60
+ if page.rotated:
61
+ utilo.verbose(f'no chapter {page.page}: rotated')
62
+ return True
63
+ pagestart = page.before(FIRST_QUARTER)
64
+ if no_textcontent(pagestart):
65
+ utilo.verbose(f'no chapter {page.page}: no textcontent')
66
+ return True
67
+ if contains_listof(pagestart):
68
+ if not capita.chapter.starter.startwith_hugenumber(pagestart):
69
+ utilo.verbose(f'no chapter {page.page}: list of dots')
70
+ # TODO: See todo below XXX???
71
+ # chapter_rate = 0
72
+ return True
73
+ return False
74
+
75
+
76
+ def no_textcontent(content: list) -> bool:
77
+ """A chapter start requires some text line at the start.
78
+
79
+ This is required to skip false positive chapter starts which are
80
+ just a table or so.
81
+ """
82
+ if len(content) < 5:
83
+ return False
84
+ avg = statistics.mean((len(item.text) for item in content))
85
+ if avg < 35:
86
+ # TODO: HOLY VALUE
87
+ return True
88
+ return False
89
+
90
+
91
+ def chaptervalue_to_percent(chaptervalue: float, hastoc: bool) -> float:
92
+ """Convert `chaptervalue` to percent.
93
+
94
+ Args:
95
+ chaptervalue(float): value of detected features
96
+ hastoc(bool): if no toc is provided, some features can not be
97
+ processed.
98
+ Returns:
99
+ Likelihood of beeing a chapter start.?
100
+ """
101
+ # TODO: HOLY VALUES
102
+ # TODO: IMPROVE THIS CONCEPT
103
+ if not hastoc and chaptervalue >= 1.0:
104
+ return 1.0
105
+ if chaptervalue >= 2.5:
106
+ return 1.0
107
+ if chaptervalue >= 0.5:
108
+ return 0.5
109
+ return 0.0
110
+
111
+
112
+ def contains_listof(content: list) -> bool:
113
+ raw = rawcontent(content)
114
+ dots_with_spaces = raw.count('. . . .')
115
+ connected_dots = raw.count('....')
116
+ result = dots_with_spaces > 4 or connected_dots >= 3
117
+ return result
118
+
119
+
120
+ def rawcontent(content) -> str:
121
+ raw = utilo.NEWLINE.join([item.text for item in content])
122
+ raw = raw.lower()
123
+ return raw