webcode-tk 1.0.9__tar.gz → 1.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: webcode-tk
3
- Version: 1.0.9
3
+ Version: 1.1.3
4
4
  Summary:
5
5
  Home-page: https://github.com/HundredVisionsGuy/webcode-tk
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "webcode-tk"
3
- version = "1.0.9"
3
+ version = "1.1.3"
4
4
  description = ""
5
5
  authors = ["hundredvisionsguy <cwinikka@gmail.com>"]
6
6
  keywords = ["html", "validation", "css", ]
@@ -18,4 +18,4 @@ Modules exported by this package:
18
18
  - `validator_tools`: sends HTML or CSS code to the W3C Validator to check
19
19
  for errors.
20
20
  """
21
- __version__ = "1.0.9"
21
+ __version__ = "1.1.3"
@@ -726,6 +726,8 @@ class CSSAppliedTree:
726
726
 
727
727
  def __get_filename(self):
728
728
  """Extracts filename from path"""
729
+ if isinstance(self.file_path, dict):
730
+ self.file_path = self.file_path.get("file")
729
731
  self.filename = clerk.get_file_name(self.file_path)
730
732
 
731
733
  def __get_font_rules(self):
@@ -1255,9 +1257,19 @@ def does_selector_apply(element: Element, selector: str) -> bool:
1255
1257
 
1256
1258
  # If the tag is an anchor, but the selector is not - doesn't apply
1257
1259
  # link color can only be changed by a link selector
1258
- if element_name == "a" and not is_link_selector:
1259
- break
1260
- elif is_type_selector:
1260
+ if element_name == "a":
1261
+ # NOTE: turns out a descendant selector targetting an anchor won't
1262
+ # show as a link_selector
1263
+ if is_descendant_selector:
1264
+ selector_list = selector.split()
1265
+
1266
+ # Check the descendant to see if it is a link
1267
+ last_item = selector_list[-1]
1268
+ if "a" != last_item[0]:
1269
+ break
1270
+ elif not is_link_selector:
1271
+ break
1272
+ if is_type_selector:
1261
1273
  applies = element_name == sel
1262
1274
  if applies:
1263
1275
  break
@@ -244,6 +244,11 @@ def hex_to_rgb(hex_code: str) -> tuple:
244
244
  """
245
245
  hex_code = hex_code.lower()
246
246
  if "#" in hex_code[0]:
247
+ if len(hex_code) == 4:
248
+ r = hex_code[1] * 2
249
+ g = hex_code[2] * 2
250
+ b = hex_code[3] * 2
251
+ hex_code = f"#{r}{g}{b}"
247
252
  hex_code = hex_code[1:]
248
253
  r = hex_code[:2]
249
254
  g = hex_code[2:4]
@@ -434,6 +434,16 @@ def get_number_of_elements_per_file(
434
434
  return elements_per_file
435
435
 
436
436
 
437
+ def has_text_content(el: Tag) -> bool:
438
+ """returns whether an element contains text content
439
+
440
+ The test is to see if there is raw text inside the element as its
441
+ content. It must be text that is not inside of a nested tag but
442
+ is the direct text outside of any other HTML elements"""
443
+ has_text_content = False
444
+ return has_text_content
445
+
446
+
437
447
  if __name__ == "__main__":
438
448
  file_with_inline_styles = "tests/test_files/sample_with_inline_styles.html"
439
449
  markup = clerk.file_to_string(file_with_inline_styles)
@@ -0,0 +1,472 @@
1
+ """A set of tools to conduct some UX (User eXperience) checks.
2
+
3
+ As of now, I just want to check for best UX and SEO practices such
4
+ as checking for best practices in writing for the web.
5
+
6
+ The primary source of information we will begin to use are from
7
+ [Writing Compelling Digital Copy](https://www.nngroup.com/courses/writing/).
8
+
9
+ According to the article...
10
+ * Be Succinct! (Writing for the Web)
11
+ * "The 3 main guidelines for writing for the web are:
12
+ - Be succinct: write no more than 50% of the text you would have used in a
13
+ hardcopy publication
14
+ - Write for scannability: don't require users to read long continuous
15
+ blocks of text
16
+ - Use hypertext to split up long information into multiple pages"
17
+ """
18
+ import re
19
+ from collections.abc import Sequence
20
+ from typing import Union
21
+
22
+ import textatistic
23
+ from file_clerk import clerk
24
+ from textatistic import Textatistic
25
+
26
+ from webcode_tk import html_tools
27
+
28
+ DEFAULT_GOALS = {
29
+ "avg_words_sentence_range": (10, 25),
30
+ "max_sentences_per_paragraph": 4,
31
+ "min_num_single_sentence_paragraphs": 2,
32
+ "min_word_count": 400,
33
+ }
34
+ DEFAULT_EXCEEDS_GOALS = {
35
+ "max_avg_words_sentence": 20,
36
+ "max_li_per_list": 9,
37
+ "max_words_per_li": 45,
38
+ "max_words_per_paragraph": 80,
39
+ "min_word_count": 800,
40
+ }
41
+
42
+
43
+ def get_flesch_kincaid_grade_level(path: str) -> float:
44
+ """returns the required education to be able to understand the text.
45
+
46
+ We're only looking at the paragraphs (not headers or list items) and
47
+ ignoring break tags as being paragraph breaks (that could be up for
48
+ debate in the future).
49
+
50
+ This function first checks to see if it's a project folder or just a
51
+ single file and ascertains for all HTML documents if it's a project.
52
+
53
+ Args:
54
+ path: the path to the page or project that we are measuring.
55
+
56
+ Returns:
57
+ grade_level: the US grade level equivalent."""
58
+
59
+ # convert all paragraph content into a single string
60
+ paragraphs = get_all_paragraphs(path)
61
+ paragraph_text = get_paragraph_text(paragraphs)
62
+
63
+ # get stats from textatistic
64
+ r = Textatistic(paragraph_text)
65
+ grade_level = r.fleschkincaid_score
66
+
67
+ # return grade level rounded to 1 decimal point
68
+ return round(grade_level, 1)
69
+
70
+
71
+ def get_readability_stats(
72
+ path: str, elements: Union[str, Sequence] = "p"
73
+ ) -> dict:
74
+ """returns a dictionary of various readability stats.
75
+
76
+ It will collect all stats from the textastic libary as well as add
77
+ a few of my own (words per sentence, sentences per paragraph).
78
+
79
+ Args:
80
+ path: the path to the file or folder (only looks at HTML files)
81
+ elements: by default we will only look at the text from paragraph
82
+ tags. You could supply other elements in the form of a list or
83
+ tuple (e.g. ['p', 'div', 'li', 'figcaption']).
84
+
85
+ Returns:
86
+ stats: a dictionary of stats on readability metrics.
87
+ """
88
+ stats = {}
89
+
90
+ # Get text and numbers
91
+ text = get_text_from_elements(path, elements)
92
+ num_lines = len(text)
93
+
94
+ # get stats from textatistic
95
+ text = "\n".join(text)
96
+ r = Textatistic(text)
97
+
98
+ for key, value in r.scores.items():
99
+ stats[key] = round(value, 2)
100
+ stats["character_count"] = r.char_count
101
+ stats["word_count"] = r.word_count
102
+ stats["sentence_count"] = r.sent_count
103
+ stats["paragraph_count"] = num_lines
104
+ stats["words_per_sentence"] = round(r.word_count / r.sent_count, 2)
105
+ stats["sent_per_paragraph"] = round(r.sent_count / num_lines, 2)
106
+
107
+ return stats
108
+
109
+
110
+ def get_paragraph_text(paragraphs: list) -> str:
111
+ """Returns a string of all the contents of the list of paragraphs
112
+
113
+ Args:
114
+ paragraphs: a list of paragraph elements.
115
+
116
+ Returns:
117
+ paragraph_text: a string of all the content of the paragraph
118
+ elements.
119
+ """
120
+ paragraph_text = ""
121
+ for paragraph in paragraphs:
122
+ paragraph_text += html_tools.get_element_content(paragraph) + "\n"
123
+ return paragraph_text.strip()
124
+
125
+
126
+ def get_all_paragraphs(path: str) -> list:
127
+ """returns a list of paragraph elements from a single file or project
128
+ folder.
129
+
130
+ It checks the path to determine if it's a path to a single page or a
131
+ project folder.
132
+
133
+ Args:
134
+ path: a string path to either an html document or a project folder.
135
+
136
+ Returns:
137
+ paragraphs: a list of paragraph elements."""
138
+
139
+ paragraphs = []
140
+ is_single_page = "html" == clerk.get_file_type(path)
141
+
142
+ if not is_single_page:
143
+ # it's a project, so we need to process all html files in the folder
144
+ all_files = clerk.get_all_files_of_type(path, "html")
145
+ for file in all_files:
146
+ paragraphs += html_tools.get_elements("p", file)
147
+ else:
148
+ paragraphs += html_tools.get_elements("p", path)
149
+ return paragraphs
150
+
151
+
152
+ def get_text_from_elements(
153
+ path: str, elements: Union[str, Sequence] = "p"
154
+ ) -> list:
155
+ """returns a list of text from elements (as strings) from a single file or
156
+ project folder.
157
+
158
+ It checks the path to determine if it's a path to a single page or a
159
+ project folder. It then uses the elements to determine from which
160
+ HTML element it will grab paragraphs.
161
+
162
+ By default, it will only pull paragraphs from the `<p>` tag, but you
163
+ can specify other elements (e.g. `<div>`, `<li>`, `<figcaption>`, etc.).
164
+
165
+ NOTE: When selecting other tags, pass it a list or tuple of strings. Only
166
+ specify the element without angle brackets (e.g. `['p', 'li', 'div']`)
167
+
168
+ Args:
169
+ path: a string path to either an html document or a project folder.
170
+ elements: the elements we want to pull our paragrphs from.
171
+ Returns:
172
+ paragraphs: a list of strings - text only without markup (for example
173
+ if an anchor is nested in a pargraph, the markup will be extracted, and
174
+ only the visible text on the page will be present."""
175
+
176
+ paragraphs = []
177
+ is_single_page = "html" == clerk.get_file_type(path)
178
+ if elements == "p":
179
+ elements = ["p"]
180
+ if not is_single_page:
181
+ # it's a project, so we need to process all html files in the folder
182
+ all_files = clerk.get_all_files_of_type(path, "html")
183
+ for file in all_files:
184
+ for element in elements:
185
+ markup = html_tools.get_elements(element, file)
186
+ for tag in markup:
187
+ text = extract_text(tag)
188
+ paragraphs.append(text)
189
+ else:
190
+ for element in elements:
191
+ markup = html_tools.get_elements(element, path)
192
+ for tag in markup:
193
+ text = extract_text(tag)
194
+ paragraphs.append(text)
195
+ return paragraphs
196
+
197
+
198
+ def remove_extensions(text: str) -> str:
199
+ """removes extension from filenames.
200
+
201
+ Filenames with extensions end up causing textatistic to count filenames
202
+ as two words instead of one, and we don't really need to count the
203
+ extensions when calculating readability stats.
204
+
205
+ Args:
206
+ text: the text which may or may not have filenames in them.
207
+
208
+ Returns:
209
+ newtext: the text without file extensions.
210
+ """
211
+ newtext = ""
212
+
213
+ # get the last character just in case it gets dropped with extension.
214
+ last_char = text[-1]
215
+ regex = r"\.[a-zA-Z0-9]+"
216
+ newtext = re.sub(regex, "", text)
217
+
218
+ # add back the last character if it was dropped.
219
+ if newtext[-1] != last_char:
220
+ newtext += last_char
221
+ return newtext
222
+
223
+
224
+ def extract_text(tag) -> str:
225
+ """extracts only the text from a tag (no markup)
226
+
227
+ Args:
228
+ tag: this is a Tag (bs4)
229
+ Returns:
230
+ text: just the visible text from the element (with no
231
+ nested markup)."""
232
+ text = tag.text
233
+ text = " ".join(text.split())
234
+ return text
235
+
236
+
237
+ def get_words_per_paragraph(path: str) -> float:
238
+ """returns average number of words per paragraph
239
+
240
+ uses Textatistic stats"""
241
+ paragraphs = get_all_paragraphs(path)
242
+ text = get_paragraph_text(paragraphs)
243
+ words = get_word_count(text)
244
+ num_paragraphs = len(paragraphs)
245
+ return round(words / num_paragraphs, 1)
246
+
247
+
248
+ def get_word_count(txt: str) -> int:
249
+ """returns word count of text.
250
+
251
+ Args:
252
+ txt: the raw text (may or may not contain line returns)
253
+
254
+ Returns:
255
+ word_count: number of words as measured by Textastic
256
+ """
257
+
258
+ text = txt.replace("\n", " ")
259
+ word_count = textatistic.word_count(text)
260
+ return word_count
261
+
262
+
263
+ def check_li_count(filename: str, list_type: str, max: int, min=3) -> list:
264
+ """returns any warnings regarding li count
265
+
266
+ Warns if too many lis in a list, too few in a list (default is 3), or
267
+ too much content in a list item.
268
+
269
+ Args:
270
+ filename: name of the HTML document.
271
+ list_type: "ul" or "li" - we don't check definition lists.
272
+ max: maximum recommended list items per list.
273
+ min: minimum recommend list items per list (default is 3)
274
+
275
+ Returns"""
276
+ warnings = []
277
+ uls = html_tools.get_elements(list_type, filename)
278
+ if uls:
279
+ num_lis = 0
280
+ for ul in uls:
281
+ lis = html_tools.get_element_content(ul)
282
+ li_count = lis.count("<li>")
283
+ num_lis += li_count
284
+ if num_lis > max:
285
+ msg = f"warning: ul should have less than {max} "
286
+ msg += f"lis per ul - you have {num_lis} in {filename}"
287
+ warnings.append(msg)
288
+ return warnings
289
+
290
+
291
+ def check_li_word_count(filepath: str, max=45) -> list:
292
+ """sends warnings if too many words are in a list item.
293
+
294
+ Args:
295
+ filepath: relative path to the HTML document in question.
296
+ max: maximum number of words per li (default is 45)
297
+
298
+ Returns:
299
+ warnings: any warnings about the li content having too many words.
300
+ """
301
+ warnings = []
302
+ filename = clerk.get_file_name(filepath)
303
+ lis = html_tools.get_elements("li", filepath)
304
+ for li in lis:
305
+ txt = html_tools.get_element_content(li)
306
+ text = txt.replace("\n", " ")
307
+ word_count = textatistic.word_count(text)
308
+ if word_count > max:
309
+ msg = "warning: An <li> element exceeds recommended word"
310
+ msg += f" count of {max} {filename} has an <li> with "
311
+ msg += f"{word_count} words."
312
+ warnings.append(msg)
313
+ return warnings
314
+
315
+
316
+ def get_usability_report(
317
+ project_dir: str,
318
+ elements: tuple,
319
+ goals=DEFAULT_GOALS,
320
+ exceeds=DEFAULT_EXCEEDS_GOALS,
321
+ ) -> list:
322
+ """Generates a list of pass, fail, and warnings on usability.
323
+
324
+ If goals or exceeds not set, goals & exceeds should follow the example
325
+ keys...
326
+ goals avg_words_sentence_range, max_sentences_per_paragraph,
327
+ min_word_count, min_num_single_sentence_paragraphs
328
+
329
+ exceeds max_avg_words_sentence, max_li_per_list, max_sentence_paragraph,
330
+ max_words_per_li, max_words_per_paragraph, min_word_count
331
+
332
+ Args:
333
+ project_dir: path to project folder
334
+ goals: set of goals to test for.
335
+ exceeds: set of metrics to determine if content goes above & beyond.
336
+ elements: set of elements to capture
337
+
338
+ Returns:
339
+ report: a list of pass, fails, and warnings
340
+ """
341
+ report = []
342
+ readability_report = get_readability_stats(project_dir, elements)
343
+ all_html_files = clerk.get_all_files_of_type(project_dir, "html")
344
+ if not all_html_files and ".html" == project_dir[-5:]:
345
+ all_html_files = [
346
+ project_dir,
347
+ ]
348
+ for type, expected in goals.items():
349
+ if type == "avg_words_sentence_range":
350
+ words_per_sentence = readability_report.get("words_per_sentence")
351
+ min, max = expected
352
+ msg = ""
353
+ if words_per_sentence <= min:
354
+ msg = "fail: not enough average words per sentence (actual = "
355
+ msg += f"{words_per_sentence})."
356
+ elif words_per_sentence >= max:
357
+ msg = f"fail: sentences are too long (should be between {min}"
358
+ msg += f" & {max}) (actual = {words_per_sentence} - should be"
359
+ msg += f" no more than {max})"
360
+ else:
361
+ msg += f"pass: at {words_per_sentence} words per sentence, "
362
+ msg += f"you are within expected range ({min}-{max} wps)."
363
+ report.append(msg)
364
+ if type == "max_sentences_per_paragraph":
365
+ max_sent = expected
366
+ min_single_sentence_data = []
367
+ single_sentences = 0
368
+ for file in all_html_files:
369
+ paragraphs = html_tools.get_elements("p", file)
370
+ para_num = 0
371
+ filename = clerk.get_file_name(file)
372
+ for paragraph in paragraphs:
373
+ para_num += 1
374
+ content = html_tools.get_element_content(paragraph).strip()
375
+ sentence_count = textatistic.sent_count(content)
376
+ if content and sentence_count == 0:
377
+ sentence_count = 1
378
+ if sentence_count == 1:
379
+ single_sentences += 1
380
+ if sentence_count > max_sent:
381
+ sentence_msg = sentence_count - max_sent
382
+ if sentence_msg == 1:
383
+ sentence_msg = f"{sentence_msg} more sentence"
384
+ else:
385
+ sentence_msg = f"{sentence_msg} more sentences"
386
+ msg = f"fail: in file, {filename}, paragraph #"
387
+ msg += f"{para_num}, you have {sentence_msg} than the"
388
+ msg += f" maximum number of {max_sent}."
389
+ report.append(msg)
390
+ min_single_sentence_data.append((filename, single_sentences))
391
+ if type == "min_word_count":
392
+ num_words = readability_report.get("word_count")
393
+ if num_words >= expected:
394
+ msg = f"pass: {filename} at {num_words} words has enough "
395
+ msg += "words to effectively measure readability."
396
+ else:
397
+ msg = f"fail: {filename} at only {num_words} words, does not "
398
+ msg += "have enough words to effectively measure readability."
399
+ report.append(msg)
400
+ if type == "min_num_single_sentence_paragraphs":
401
+ for datum in min_single_sentence_data:
402
+ name, paras_with_one_sentence = datum
403
+ if paras_with_one_sentence >= expected:
404
+ msg = f"pass: {name} has enough paragraphs with just one"
405
+ msg += " sentence."
406
+ else:
407
+ msg = f"fail: {name} does not have enough paragraphs with"
408
+ msg += " a single sentence. You should have at least"
409
+ msg += f" {expected}."
410
+ report.append(msg)
411
+
412
+ # Get List data for exceeds:
413
+ li_report = []
414
+ for file in all_html_files:
415
+ filename = clerk.get_file_name(file)
416
+ max_li_per_ul = exceeds.get("max_li_per_list")
417
+ li_report += check_li_count(file, "ul", max_li_per_ul)
418
+ li_report += check_li_word_count(file)
419
+
420
+ for type, goal in exceeds.items():
421
+ if type == "max_avg_words_sentence":
422
+ if words_per_sentence > 20:
423
+ warning = "warning: Average words per sentence should be 20 "
424
+ warning += "or less (you have an average of "
425
+ warning += f"{words_per_sentence})."
426
+ report.append(warning)
427
+ if type == "max_li_per_list":
428
+ if li_report:
429
+ for item in li_report:
430
+ report.append(item)
431
+ if type == "max_sentence_paragraph":
432
+ if sentence_count > goal:
433
+ warning = "warning: for best results, you should have fewer "
434
+ warning += f"than {goal} sentences per paragraph, but you "
435
+ warning += f"have {sentence_count}."
436
+ if type == "max_words_per_li":
437
+ if li_report:
438
+ for item in li_report:
439
+ if "An <li> element exceeds recommended" in item:
440
+ report.append(item)
441
+ if type == "max_words_per_paragraph":
442
+ paragraphs_exceeding = 0
443
+ maximum_num_words = 0
444
+ for paragraph in paragraphs:
445
+ content = html_tools.get_element_content(paragraph)
446
+ word_count = textatistic.word_count(content)
447
+ if word_count > expected:
448
+ paragraphs_exceeding += 1
449
+ if word_count > maximum_num_words:
450
+ maximum_num_words = word_count
451
+ if paragraphs_exceeding:
452
+ msg = f"warning: you should not exceed {expected} words per "
453
+ msg += f"paragraph, but you have {paragraphs_exceeding} "
454
+ msg += "paragraphs that exceed, the maximum having "
455
+ msg += f"{maximum_num_words} words."
456
+ report.append(msg)
457
+ if type == "min_word_count":
458
+ # warn if meets minimum word count but not the goal for exceeds
459
+ min_words = goals.get("min_word_count")
460
+ if num_words > min_words and num_words < goal:
461
+ msg = "warning: for best results, you should have at least"
462
+ msg += f" {goal} words in your project."
463
+ report.append(msg)
464
+ return report
465
+
466
+
467
+ if __name__ == "__main__":
468
+ # let's test some stuff out.
469
+ path = "tests/test_files/large_project/"
470
+ all = extract_text(path, ["p", "figcaption"])
471
+ all = get_paragraph_text(all)
472
+ print(all)
@@ -1,240 +0,0 @@
1
- """A set of tools to conduct some UX (User eXperience) checks.
2
-
3
- As of now, I just want to check for best UX and SEO practices such
4
- as checking for best practices in writing for the web.
5
-
6
- The primary source of information we will begin to use are from
7
- [Writing Compelling Digital Copy](https://www.nngroup.com/courses/writing/).
8
-
9
- According to the article...
10
- * Be Succinct! (Writing for the Web)
11
- * "The 3 main guidelines for writing for the web are:
12
- - Be succinct: write no more than 50% of the text you would have used in a
13
- hardcopy publication
14
- - Write for scannability: don't require users to read long continuous
15
- blocks of text
16
- - Use hypertext to split up long information into multiple pages"
17
- """
18
- import re
19
- from collections.abc import Sequence
20
- from typing import Union
21
-
22
- from file_clerk import clerk
23
- from textatistic import Textatistic
24
-
25
- from webcode_tk import html_tools
26
-
27
-
28
- def get_flesch_kincaid_grade_level(path: str) -> float:
29
- """returns the required education to be able to understand the text.
30
-
31
- We're only looking at the paragraphs (not headers or list items) and
32
- ignoring break tags as being paragraph breaks (that could be up for
33
- debate in the future).
34
-
35
- This function first checks to see if it's a project folder or just a
36
- single file and ascertains for all HTML documents if it's a project.
37
-
38
- Args:
39
- path: the path to the page or project that we are measuring.
40
-
41
- Returns:
42
- grade_level: the US grade level equivalent."""
43
-
44
- # convert all paragraph content into a single string
45
- paragraphs = get_all_paragraphs(path)
46
- paragraph_text = get_paragraph_text(paragraphs)
47
-
48
- # get stats from textatistic
49
- r = Textatistic(paragraph_text)
50
- grade_level = r.fleschkincaid_score
51
-
52
- # return grade level rounded to 1 decimal point
53
- return round(grade_level, 1)
54
-
55
-
56
- def get_readability_stats(
57
- path: str, elements: Union[str, Sequence] = "p"
58
- ) -> dict:
59
- """returns a dictionary of various readability stats.
60
-
61
- It will collect all stats from the textastic libary as well as add
62
- a few of my own (words per sentence, sentences per paragraph).
63
-
64
- Args:
65
- path: the path to the file or folder (only looks at HTML files)
66
- elements: by default we will only look at the text from paragraph
67
- tags. You could supply other elements in the form of a list or
68
- tuple (e.g. ['p', 'div', 'li', 'figcaption']).
69
-
70
- Returns:
71
- stats: a dictionary of stats on readability metrics.
72
- """
73
- stats = {}
74
-
75
- # Get text and numbers
76
- text = get_text_from_elements(path, elements)
77
- num_lines = len(text)
78
-
79
- # get stats from textatistic
80
- text = "\n".join(text)
81
- r = Textatistic(text)
82
-
83
- for key, value in r.scores.items():
84
- stats[key] = round(value, 2)
85
- stats["character_count"] = r.char_count
86
- stats["word_count"] = r.word_count
87
- stats["sentence_count"] = r.sent_count
88
- stats["paragraph_count"] = num_lines
89
- stats["words_per_sentence"] = round(r.word_count / r.sent_count, 2)
90
- stats["sent_per_paragraph"] = round(r.sent_count / num_lines, 2)
91
-
92
- return stats
93
-
94
-
95
- def get_paragraph_text(paragraphs: list) -> str:
96
- """Returns a string of all the contents of the list of paragraphs
97
-
98
- Args:
99
- paragraphs: a list of paragraph elements.
100
-
101
- Returns:
102
- paragraph_text: a string of all the content of the paragraph
103
- elements.
104
- """
105
- paragraph_text = ""
106
- for paragraph in paragraphs:
107
- paragraph_text += html_tools.get_element_content(paragraph) + "\n"
108
- return paragraph_text.strip()
109
-
110
-
111
- def get_all_paragraphs(path: str) -> list:
112
- """returns a list of paragraph elements from a single file or project
113
- folder.
114
-
115
- It checks the path to determine if it's a path to a single page or a
116
- project folder.
117
-
118
- Args:
119
- path: a string path to either an html document or a project folder.
120
-
121
- Returns:
122
- paragraphs: a list of paragraph elements."""
123
-
124
- paragraphs = []
125
- is_single_page = "html" == clerk.get_file_type(path)
126
-
127
- if not is_single_page:
128
- # it's a project, so we need to process all html files in the folder
129
- all_files = clerk.get_all_files_of_type(path, "html")
130
- for file in all_files:
131
- paragraphs += html_tools.get_elements("p", file)
132
- else:
133
- paragraphs += html_tools.get_elements("p", path)
134
- return paragraphs
135
-
136
-
137
- def get_text_from_elements(
138
- path: str, elements: Union[str, Sequence] = "p"
139
- ) -> list:
140
- """returns a list of text from elements (as strings) from a single file or
141
- project folder.
142
-
143
- It checks the path to determine if it's a path to a single page or a
144
- project folder. It then uses the elements to determine from which
145
- HTML element it will grab paragraphs.
146
-
147
- By default, it will only pull paragraphs from the `<p>` tag, but you
148
- can specify other elements (e.g. `<div>`, `<li>`, `<figcaption>`, etc.).
149
-
150
- NOTE: When selecting other tags, pass it a list or tuple of strings. Only
151
- specify the element without angle brackets (e.g. `['p', 'li', 'div']`)
152
-
153
- Args:
154
- path: a string path to either an html document or a project folder.
155
- elements: the elements we want to pull our paragrphs from.
156
- Returns:
157
- paragraphs: a list of strings - text only without markup (for example
158
- if an anchor is nested in a pargraph, the markup will be extracted, and
159
- only the visible text on the page will be present."""
160
-
161
- paragraphs = []
162
- is_single_page = "html" == clerk.get_file_type(path)
163
- if elements == "p":
164
- elements = ["p"]
165
- if not is_single_page:
166
- # it's a project, so we need to process all html files in the folder
167
- all_files = clerk.get_all_files_of_type(path, "html")
168
- for file in all_files:
169
- for element in elements:
170
- markup = html_tools.get_elements(element, file)
171
- for tag in markup:
172
- text = extract_text(tag)
173
- paragraphs.append(text)
174
- else:
175
- for element in elements:
176
- markup = html_tools.get_elements(element, path)
177
- for tag in markup:
178
- text = extract_text(tag)
179
- paragraphs.append(text)
180
- return paragraphs
181
-
182
-
183
- def remove_extensions(text: str) -> str:
184
- """removes extension from filenames.
185
-
186
- Filenames with extensions end up causing textatistic to count filenames
187
- as two words instead of one, and we don't really need to count the
188
- extensions when calculating readability stats.
189
-
190
- Args:
191
- text: the text which may or may not have filenames in them.
192
-
193
- Returns:
194
- newtext: the text without file extensions.
195
- """
196
- newtext = ""
197
-
198
- # get the last character just in case it gets dropped with extension.
199
- last_char = text[-1]
200
- regex = r"\.[a-zA-Z0-9]+"
201
- newtext = re.sub(regex, "", text)
202
-
203
- # add back the last character if it was dropped.
204
- if newtext[-1] != last_char:
205
- newtext += last_char
206
- return newtext
207
-
208
-
209
- def extract_text(tag) -> str:
210
- """extracts only the text from a tag (no markup)
211
-
212
- Args:
213
- tag: this is a Tag (bs4)
214
- Returns:
215
- text: just the visible text from the element (with no
216
- nested markup)."""
217
- text = tag.text
218
- text = " ".join(text.split())
219
- return text
220
-
221
-
222
- def get_words_per_paragraph(path: str) -> float:
223
- """returns average number of words per paragraph
224
-
225
- uses Textatistic stats"""
226
- paragraphs = get_all_paragraphs(path)
227
- text = get_paragraph_text(paragraphs)
228
- text = text.replace("\n", " ")
229
- r = Textatistic(text)
230
- words = r.counts.get("word_count")
231
- num_paragraphs = len(paragraphs)
232
- return round(words / num_paragraphs, 1)
233
-
234
-
235
- if __name__ == "__main__":
236
- # let's test some stuff out.
237
- path = "tests/test_files/large_project/"
238
- all = extract_text(path, ["p", "figcaption"])
239
- all = get_paragraph_text(all)
240
- print(all)
File without changes
File without changes