simplibs-extractor-html 0.1.0__py3-none-any.whl

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 (127) hide show
  1. simplibs/extractor_html/__init__.py +82 -0
  2. simplibs/extractor_html/actions/ApplyToHtmlBlocks.py +29 -0
  3. simplibs/extractor_html/actions/CleanupHtmlWhitespace.py +22 -0
  4. simplibs/extractor_html/actions/CompactHtml.py +22 -0
  5. simplibs/extractor_html/actions/HtmlOutline.py +23 -0
  6. simplibs/extractor_html/actions/RemoveTemplateImports.py +22 -0
  7. simplibs/extractor_html/actions/StripHtmlComments.py +23 -0
  8. simplibs/extractor_html/actions/StripTemplateComments.py +27 -0
  9. simplibs/extractor_html/actions/TemplateOutline.py +23 -0
  10. simplibs/extractor_html/actions/__init__.py +57 -0
  11. simplibs/extractor_html/annotations/TemplateEngine.py +26 -0
  12. simplibs/extractor_html/annotations/__init__.py +14 -0
  13. simplibs/extractor_html/constants/HTML_ANY_TAG_PATTERN.py +16 -0
  14. simplibs/extractor_html/constants/HTML_BLOCK_LANGUAGE_BY_TYPE.py +41 -0
  15. simplibs/extractor_html/constants/HTML_COMMENT_END_PATTERN.py +19 -0
  16. simplibs/extractor_html/constants/HTML_CONDITIONAL_PATTERN.py +23 -0
  17. simplibs/extractor_html/constants/HTML_DEPENDENCY_DIR_NAMES.py +18 -0
  18. simplibs/extractor_html/constants/HTML_EXTRA_LANGUAGE_BY_SUFFIX.py +31 -0
  19. simplibs/extractor_html/constants/HTML_HEADING_ELEMENTS.py +15 -0
  20. simplibs/extractor_html/constants/HTML_ID_ATTRIBUTE_PATTERN.py +21 -0
  21. simplibs/extractor_html/constants/HTML_IMPLIED_END.py +34 -0
  22. simplibs/extractor_html/constants/HTML_LICENSE_START.py +14 -0
  23. simplibs/extractor_html/constants/HTML_LICENSE_TAGS.py +14 -0
  24. simplibs/extractor_html/constants/HTML_OUTLINE_ELEMENTS.py +32 -0
  25. simplibs/extractor_html/constants/HTML_OUTLINE_TEXT_LIMIT.py +15 -0
  26. simplibs/extractor_html/constants/HTML_OUTPUT_DIR_NAMES.py +43 -0
  27. simplibs/extractor_html/constants/HTML_PLAIN_SUFFIXES.py +18 -0
  28. simplibs/extractor_html/constants/HTML_RAW_TEXT_END_PATTERNS.py +28 -0
  29. simplibs/extractor_html/constants/HTML_SUFFIXES.py +22 -0
  30. simplibs/extractor_html/constants/HTML_TAG_NAME_PATTERN.py +16 -0
  31. simplibs/extractor_html/constants/HTML_TEMPLATE_BLOCKS.py +38 -0
  32. simplibs/extractor_html/constants/HTML_TEMPLATE_IMPORT_TAGS.py +29 -0
  33. simplibs/extractor_html/constants/HTML_TEMPLATE_OUTLINE_TAGS.py +35 -0
  34. simplibs/extractor_html/constants/HTML_TEMPLATE_SUFFIXES.py +19 -0
  35. simplibs/extractor_html/constants/HTML_TEMPLATE_TAG_NAME_PATTERN.py +16 -0
  36. simplibs/extractor_html/constants/HTML_TEMPLATE_TOKEN_PATTERN.py +16 -0
  37. simplibs/extractor_html/constants/HTML_TEXT_ELEMENTS.py +16 -0
  38. simplibs/extractor_html/constants/HTML_TOKEN_PATTERN.py +26 -0
  39. simplibs/extractor_html/constants/HTML_TYPE_ATTRIBUTE_PATTERN.py +21 -0
  40. simplibs/extractor_html/constants/HTML_VOID_ELEMENTS.py +21 -0
  41. simplibs/extractor_html/constants/HTML_WRAPPED_STEM_ENDING.py +14 -0
  42. simplibs/extractor_html/constants/HTML_WRAPPED_SUFFIXES.py +15 -0
  43. simplibs/extractor_html/constants/__init__.py +77 -0
  44. simplibs/extractor_html/file_rules/__init__.py +56 -0
  45. simplibs/extractor_html/file_rules/atomic/DEPENDENCY_DIRS.py +17 -0
  46. simplibs/extractor_html/file_rules/atomic/DOT_NAME.py +19 -0
  47. simplibs/extractor_html/file_rules/atomic/OUTPUT_DIRS.py +17 -0
  48. simplibs/extractor_html/file_rules/atomic/VCS_DIRS.py +21 -0
  49. simplibs/extractor_html/file_rules/atomic/WRAPPED_HTML.py +22 -0
  50. simplibs/extractor_html/file_rules/atomic/__init__.py +26 -0
  51. simplibs/extractor_html/file_rules/html/HTML_BASE.py +22 -0
  52. simplibs/extractor_html/file_rules/html/HTML_FILES.py +24 -0
  53. simplibs/extractor_html/file_rules/html/HTML_PLAIN_BASE.py +24 -0
  54. simplibs/extractor_html/file_rules/html/HTML_STRICT_EXCLUDE.py +21 -0
  55. simplibs/extractor_html/file_rules/html/__init__.py +20 -0
  56. simplibs/extractor_html/logic/__init__.py +44 -0
  57. simplibs/extractor_html/logic/_helpers/HtmlBlock.py +31 -0
  58. simplibs/extractor_html/logic/_helpers/HtmlOutlineBuilder.py +189 -0
  59. simplibs/extractor_html/logic/_helpers/HtmlScan.py +50 -0
  60. simplibs/extractor_html/logic/_helpers/HtmlTag.py +32 -0
  61. simplibs/extractor_html/logic/_helpers/Ranges.py +48 -0
  62. simplibs/extractor_html/logic/_helpers/TemplateScan.py +43 -0
  63. simplibs/extractor_html/logic/_helpers/TemplateTag.py +33 -0
  64. simplibs/extractor_html/logic/_helpers/__init__.py +68 -0
  65. simplibs/extractor_html/logic/_helpers/blank_spans.py +40 -0
  66. simplibs/extractor_html/logic/_helpers/build_html_outline.py +38 -0
  67. simplibs/extractor_html/logic/_helpers/build_template_outline.py +75 -0
  68. simplibs/extractor_html/logic/_helpers/find_blanks_start.py +29 -0
  69. simplibs/extractor_html/logic/_helpers/find_block_language.py +35 -0
  70. simplibs/extractor_html/logic/_helpers/find_comments.py +60 -0
  71. simplibs/extractor_html/logic/_helpers/find_protected_spans.py +48 -0
  72. simplibs/extractor_html/logic/_helpers/find_template_import_spans.py +37 -0
  73. simplibs/extractor_html/logic/_helpers/fit_removal_span.py +40 -0
  74. simplibs/extractor_html/logic/_helpers/is_license_comment.py +26 -0
  75. simplibs/extractor_html/logic/_helpers/line_of.py +23 -0
  76. simplibs/extractor_html/logic/_helpers/raise_block_result_invalid.py +38 -0
  77. simplibs/extractor_html/logic/_helpers/replace_spans.py +42 -0
  78. simplibs/extractor_html/logic/_helpers/scan_checked.py +45 -0
  79. simplibs/extractor_html/logic/_helpers/scan_html.py +208 -0
  80. simplibs/extractor_html/logic/_helpers/scan_templates.py +128 -0
  81. simplibs/extractor_html/logic/_helpers/scan_templates_checked.py +39 -0
  82. simplibs/extractor_html/logic/_helpers/skip_html_comment.py +41 -0
  83. simplibs/extractor_html/logic/_helpers/skip_html_tag.py +46 -0
  84. simplibs/extractor_html/logic/_helpers/tidy_lines.py +86 -0
  85. simplibs/extractor_html/logic/_helpers/warn_problems.py +32 -0
  86. simplibs/extractor_html/logic/apply_to_html_blocks.py +103 -0
  87. simplibs/extractor_html/logic/cleanup_html_whitespace.py +54 -0
  88. simplibs/extractor_html/logic/compact_html.py +44 -0
  89. simplibs/extractor_html/logic/html_comment_spans.py +54 -0
  90. simplibs/extractor_html/logic/html_outline.py +66 -0
  91. simplibs/extractor_html/logic/strip_html_comments.py +63 -0
  92. simplibs/extractor_html/logic/templates/__init__.py +25 -0
  93. simplibs/extractor_html/logic/templates/remove_template_imports.py +51 -0
  94. simplibs/extractor_html/logic/templates/strip_template_comments.py +60 -0
  95. simplibs/extractor_html/logic/templates/template_comment_spans.py +49 -0
  96. simplibs/extractor_html/logic/templates/template_outline.py +62 -0
  97. simplibs/extractor_html/plugin/__init__.py +14 -0
  98. simplibs/extractor_html/plugin/register_all.py +60 -0
  99. simplibs/extractor_html/presets/__init__.py +43 -0
  100. simplibs/extractor_html/presets/html_clean_steps.py +54 -0
  101. simplibs/extractor_html/presets/html_no_imports_steps.py +50 -0
  102. simplibs/extractor_html/presets/html_outline_steps.py +28 -0
  103. simplibs/extractor_html/presets/template_structure_steps.py +27 -0
  104. simplibs/extractor_html/profiles/DJANGO_CLEAN.py +36 -0
  105. simplibs/extractor_html/profiles/DJANGO_NO_IMPORTS.py +32 -0
  106. simplibs/extractor_html/profiles/DJANGO_OUTLINE.py +32 -0
  107. simplibs/extractor_html/profiles/DJANGO_STRUCTURE.py +32 -0
  108. simplibs/extractor_html/profiles/HTML_CODE_CLEAN.py +32 -0
  109. simplibs/extractor_html/profiles/HTML_CODE_NO_IMPORTS.py +30 -0
  110. simplibs/extractor_html/profiles/HTML_DATA_ALL.py +26 -0
  111. simplibs/extractor_html/profiles/HTML_OUTLINE.py +31 -0
  112. simplibs/extractor_html/profiles/HTML_PROFILES.py +42 -0
  113. simplibs/extractor_html/profiles/HTML_SRC_CLEAN.py +31 -0
  114. simplibs/extractor_html/profiles/JINJA_CLEAN.py +35 -0
  115. simplibs/extractor_html/profiles/JINJA_NO_IMPORTS.py +32 -0
  116. simplibs/extractor_html/profiles/JINJA_OUTLINE.py +30 -0
  117. simplibs/extractor_html/profiles/JINJA_STRUCTURE.py +31 -0
  118. simplibs/extractor_html/profiles/__init__.py +86 -0
  119. simplibs/extractor_html/profiles/steps_for_engine.py +39 -0
  120. simplibs/extractor_html/profiles/steps_for_pages.py +36 -0
  121. simplibs/extractor_html/py.typed +1 -0
  122. simplibs_extractor_html-0.1.0.dist-info/METADATA +504 -0
  123. simplibs_extractor_html-0.1.0.dist-info/RECORD +127 -0
  124. simplibs_extractor_html-0.1.0.dist-info/WHEEL +5 -0
  125. simplibs_extractor_html-0.1.0.dist-info/entry_points.txt +2 -0
  126. simplibs_extractor_html-0.1.0.dist-info/licenses/LICENSE +21 -0
  127. simplibs_extractor_html-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,68 @@
1
+ from .HtmlBlock import HtmlBlock
2
+ from .HtmlOutlineBuilder import HtmlOutlineBuilder
3
+ from .HtmlScan import HtmlScan
4
+ from .HtmlTag import HtmlTag
5
+ from .Ranges import Ranges
6
+ from .TemplateScan import TemplateScan
7
+ from .TemplateTag import TemplateTag
8
+ from .blank_spans import blank_spans
9
+ from .build_html_outline import build_html_outline
10
+ from .build_template_outline import build_template_outline
11
+ from .find_blanks_start import find_blanks_start
12
+ from .find_block_language import find_block_language
13
+ from .find_comments import find_comments
14
+ from .find_protected_spans import find_protected_spans
15
+ from .find_template_import_spans import find_template_import_spans
16
+ from .fit_removal_span import fit_removal_span
17
+ from .is_license_comment import is_license_comment
18
+ from .line_of import line_of
19
+ from .raise_block_result_invalid import raise_block_result_invalid
20
+ from .replace_spans import replace_spans
21
+ from .scan_checked import scan_checked
22
+ from .scan_html import scan_html
23
+ from .scan_templates import scan_templates
24
+ from .scan_templates_checked import scan_templates_checked
25
+ from .skip_html_comment import skip_html_comment
26
+ from .skip_html_tag import skip_html_tag
27
+ from .tidy_lines import tidy_lines
28
+ from .warn_problems import warn_problems
29
+
30
+
31
+ _DESIGN_NOTES = """
32
+ # _helpers — The Scanners and Their Parts
33
+
34
+ Private. Plain imports, no `__all__`.
35
+
36
+ ## Internal Components Registry
37
+
38
+ | Component | Type | Description |
39
+ | :--------------------------- | :------- | :------------------------------------------------------------------- |
40
+ | `HtmlBlock` | Class | The content of a `script` or `style` element, and its language. |
41
+ | `HtmlOutlineBuilder` | Class | Follows the tags and writes the outline of a page. |
42
+ | `HtmlScan` | Class | Comments, blocks, preserved ranges, tags and problems of one pass. |
43
+ | `HtmlTag` | Class | One start tag or end tag: name, direction, span. |
44
+ | `Ranges` | Class | Ranges that a stretch of the text can be asked to touch. |
45
+ | `TemplateScan` | Class | Template comments, verbatim blocks, tags and the blanked text. |
46
+ | `TemplateTag` | Class | One `{% ... %}` tag: name, span, arguments. |
47
+ | `blank_spans` | Function | Replaces ranges with blanks, keeping every offset. |
48
+ | `build_html_outline` | Function | The outline of a page from the tags of its scan. |
49
+ | `build_template_outline` | Function | The skeleton of a template from its tags. |
50
+ | `find_blanks_start` | Function | Moves back over the blanks in front of a comment. |
51
+ | `find_block_language` | Function | Is the content of a `script` or `style` code, and of what language? |
52
+ | `find_comments` | Function | Scans and chooses the HTML comments a step cuts. |
53
+ | `find_protected_spans` | Function | The ranges where the whitespace steps change nothing. |
54
+ | `find_template_import_spans` | Function | The ranges of the import tags of a template. |
55
+ | `fit_removal_span` | Function | The cut of one comment or tag. |
56
+ | `is_license_comment` | Function | Is this a comment that tools keep? |
57
+ | `line_of` | Function | The line of an offset. |
58
+ | `raise_block_result_invalid` | Function | Reports a block step that did not return a text. |
59
+ | `replace_spans` | Function | Puts a new text in place of each of some ranges. |
60
+ | `scan_checked` | Function | Scans a page; gives `None` and warns when it looks damaged. |
61
+ | `scan_html` | Function | One pass over a page. |
62
+ | `scan_templates` | Function | One pass over the template constructs of a page. |
63
+ | `scan_templates_checked` | Function | Scans the constructs; gives `None` and warns when they look damaged. |
64
+ | `skip_html_comment` | Function | The end of a comment. |
65
+ | `skip_html_tag` | Function | The end of a tag, quoted values included. |
66
+ | `tidy_lines` | Function | Trims the line ends and reduces the blank lines, sparing protected lines. |
67
+ | `warn_problems` | Function | Turns sentences about a damaged page into warnings. |
68
+ """
@@ -0,0 +1,40 @@
1
+ from collections.abc import Iterable
2
+ from simplibs.text import Span
3
+
4
+
5
+ def blank_spans(text: str, spans: Iterable[Span]) -> str:
6
+ """Replace the characters of some ranges with spaces, keeping every line break and every offset.
7
+
8
+ Args:
9
+ text: The source.
10
+ spans: The ranges to blank (the template constructs of a scan).
11
+
12
+ Returns:
13
+ A text of the same length in which the ranges hold only spaces and line breaks.
14
+ """
15
+
16
+ # 1. Copy what is between the ranges, put blanks where the ranges were
17
+ pieces: list[str] = []
18
+ previous = 0
19
+ for start, end in sorted(spans):
20
+ pieces.append(text[previous:start])
21
+ pieces.append("".join("\n" if character == "\n" else " " for character in text[start:end]))
22
+ previous = end
23
+ pieces.append(text[previous:])
24
+
25
+ return "".join(pieces)
26
+
27
+
28
+ _DESIGN_NOTES = """
29
+ # blank_spans — Hide Without Moving
30
+
31
+ ## Purpose
32
+ A tag like `<a href="{% url 'x' %}" {% if y %}class="b"{% endif %}>` has
33
+ template constructs in the middle of it. Cutting them out would shift every
34
+ offset; blanking them leaves all offsets where they were, and the scan of the
35
+ HTML reads a tag with a few more blanks in it.
36
+
37
+ ## Line Breaks Stay
38
+ The blanked text has the same lines as the original, so a line number counted
39
+ in it is the line number in the page.
40
+ """
@@ -0,0 +1,38 @@
1
+ # Inners
2
+ from .HtmlOutlineBuilder import HtmlOutlineBuilder
3
+ from .HtmlScan import HtmlScan
4
+
5
+
6
+ def build_html_outline(text: str, scan: HtmlScan, max_depth: int | None) -> str:
7
+ """Write the outline of a page from the tags of its scan.
8
+
9
+ Args:
10
+ text: The source.
11
+ scan: The result of `scan_html` for `text`, made with `collect_tags=True`.
12
+ max_depth: The number of levels to list, or `None` for all of them.
13
+
14
+ Returns:
15
+ One line for each listed element, indented by depth; an empty string if there is none.
16
+ """
17
+
18
+ # 1. Feed the tags to the builder in the order of the page
19
+ builder = HtmlOutlineBuilder(text, max_depth)
20
+ for tag in scan.tags:
21
+ if tag.closing:
22
+ builder.end_tag(tag.name, tag.span)
23
+ else:
24
+ builder.start_tag(tag.name, tag.span)
25
+
26
+ # 2. The lines, each ended by a line break
27
+ lines = builder.lines()
28
+ return "\n".join(lines) + "\n" if lines else ""
29
+
30
+
31
+ _DESIGN_NOTES = """
32
+ # build_html_outline — From the Tags to the Lines
33
+
34
+ ## Purpose
35
+ The thin layer between the scan and the builder: it knows the order of the tags
36
+ and the format of the result (one line each, a line break at the end, nothing
37
+ for a page with nothing to list).
38
+ """
@@ -0,0 +1,75 @@
1
+ # Outers
2
+ from ...annotations import TemplateEngine
3
+ from ...constants import HTML_TEMPLATE_OUTLINE_TAGS
4
+ # Inners
5
+ from .TemplateScan import TemplateScan
6
+
7
+
8
+ def build_template_outline(
9
+ scan: TemplateScan,
10
+ engine: TemplateEngine,
11
+ max_depth: int | None,
12
+ ) -> tuple[list[str], list[str]]:
13
+ """Write the skeleton of a template: `extends`, `include` and the blocks, indented by depth.
14
+
15
+ Args:
16
+ scan: The result of `scan_templates`.
17
+ engine: `"django"` or `"jinja"`.
18
+ max_depth: The number of levels to list, or `None` for all of them.
19
+
20
+ Returns:
21
+ The lines, and the problems found (blocks that do not pair up).
22
+ """
23
+
24
+ # 1. Which tags open a level, which close it, which stand alone
25
+ names = HTML_TEMPLATE_OUTLINE_TAGS[engine]
26
+ lines: list[str] = []
27
+ depth = 0
28
+ stray = 0
29
+
30
+ for tag in scan.tags:
31
+
32
+ # 2. A tag that closes a level
33
+ if tag.name in names["closes"]:
34
+ if depth:
35
+ depth -= 1
36
+ else:
37
+ stray += 1
38
+ continue
39
+
40
+ # 3. A tag of the outline is written as the tag it is, with its arguments on one line
41
+ if tag.name in names["opens"] or tag.name in names["lines"]:
42
+ if max_depth is None or depth < max_depth:
43
+ arguments = " ".join(tag.arguments.split())
44
+ written = f"{{% {tag.name} {arguments} %}}" if arguments else f"{{% {tag.name} %}}"
45
+ lines.append(" " * depth + written)
46
+ if tag.name in names["opens"]:
47
+ depth += 1
48
+
49
+ # 4. Levels that do not pair up
50
+ problems: list[str] = []
51
+ if depth:
52
+ problems.append(f"{depth} block(s) of the template are never closed")
53
+ if stray:
54
+ problems.append(f"{stray} end tag(s) of the template have no opening one")
55
+
56
+ return lines, problems
57
+
58
+
59
+ _DESIGN_NOTES = """
60
+ # build_template_outline — What the Template Is Built Of
61
+
62
+ ## Purpose
63
+ `{% extends "base.html" %}` and the blocks around the content say what a
64
+ template is for; the rest of it is detail. The outline writes those tags as
65
+ they stand in the template, so they can be found by searching for them.
66
+
67
+ ## Nesting
68
+ `block` (and in Jinja `macro`) open a level, `endblock` (and `endmacro`) close
69
+ it. A level is counted even when it is deeper than `max_depth`, so the blocks
70
+ that follow are still on the right level.
71
+
72
+ ## Problems
73
+ Blocks that do not pair up mean the nesting cannot be trusted. The step then
74
+ leaves the template as it is, with a warning.
75
+ """
@@ -0,0 +1,29 @@
1
+ def find_blanks_start(text: str, position: int) -> int:
2
+ """Move back over the spaces and tabs in front of a position.
3
+
4
+ Args:
5
+ text: The source.
6
+ position: Where a comment starts.
7
+
8
+ Returns:
9
+ The offset of the first blank of the run that ends at `position`, or `position` itself.
10
+ """
11
+
12
+ # 1. Spaces and tabs only: a line break is never swallowed
13
+ while position > 0 and text[position - 1] in " \t":
14
+ position -= 1
15
+
16
+ return position
17
+
18
+
19
+ _DESIGN_NOTES = """
20
+ # find_blanks_start — The Blanks Go With the Comment
21
+
22
+ ## Purpose
23
+ `<p>a</p> <!-- note -->` should become `<p>a</p>`, not `<p>a</p> `. The blanks
24
+ in front of a comment belong to it, so the cut takes them along.
25
+
26
+ ## Only Spaces and Tabs
27
+ The line break in front of a comment is not touched: removing it would pull
28
+ the next line up.
29
+ """
@@ -0,0 +1,35 @@
1
+ from ...constants import HTML_BLOCK_LANGUAGE_BY_TYPE, HTML_TYPE_ATTRIBUTE_PATTERN
2
+
3
+
4
+ def find_block_language(kind: str, tag: str) -> str | None:
5
+ """Say what the content of a `script` or `style` element is.
6
+
7
+ Args:
8
+ kind: `"script"` or `"style"`.
9
+ tag: The start tag of the element, with its attributes.
10
+
11
+ Returns:
12
+ `"javascript"` or `"css"` if the `type` of the tag says the content is code; otherwise `None`.
13
+ """
14
+
15
+ # 1. The value of `type`; a tag without it means the default
16
+ match = HTML_TYPE_ATTRIBUTE_PATTERN.search(tag)
17
+ value = ""
18
+ if match is not None:
19
+ value = next(group for group in match.groups() if group is not None).strip().lower()
20
+
21
+ # 2. A known type is code of a language, anything else is data
22
+ return HTML_BLOCK_LANGUAGE_BY_TYPE[kind].get(value)
23
+
24
+
25
+ _DESIGN_NOTES = """
26
+ # find_block_language — Is the Content Code
27
+
28
+ ## Purpose
29
+ `<script type="application/ld+json">` holds JSON and `<script type="text/x-template">`
30
+ a template of a framework; running the JavaScript steps over them would
31
+ damage them. Only the types of `HTML_BLOCK_LANGUAGE_BY_TYPE` are code.
32
+
33
+ ## Case
34
+ Values of `type` are compared in lower case and without the blanks around them.
35
+ """
@@ -0,0 +1,60 @@
1
+ from simplibs.text import Span
2
+ # Outers
3
+ from ...annotations import TemplateEngine
4
+ # Inners
5
+ from .is_license_comment import is_license_comment
6
+ from .scan_checked import scan_checked
7
+
8
+
9
+ def find_comments(
10
+ text: str,
11
+ *,
12
+ engine: TemplateEngine | None,
13
+ keep_conditional: bool,
14
+ keep_license: bool,
15
+ ) -> list[Span]:
16
+ """Scan a page and choose the HTML comments a step is going to cut.
17
+
18
+ Args:
19
+ text: The source.
20
+ engine: The template language of the page; `None` for a page that is only HTML.
21
+ keep_conditional: Leave out the Internet Explorer conditional comments.
22
+ keep_license: Leave out the comments that tools keep (`<!--! ... -->`, `@license`, `@preserve`).
23
+
24
+ Returns:
25
+ The comments to cut; empty for a page that looks damaged (a warning has been given).
26
+ """
27
+
28
+ # 1. One pass; a damaged page is left alone
29
+ scan = scan_checked(text, engine=engine)
30
+ if scan is None:
31
+ return []
32
+
33
+ # 2. What is to stay is told apart by the position and by the text
34
+ kept: set[Span] = set()
35
+ if keep_conditional:
36
+ kept.update(scan.conditional)
37
+ if keep_license:
38
+ kept.update(span for span in scan.comments if is_license_comment(text[span[0]:span[1]]))
39
+
40
+ # 3. The rest goes
41
+ return [span for span in scan.comments if span not in kept]
42
+
43
+
44
+ _DESIGN_NOTES = """
45
+ # find_comments — The One Place That Chooses the Comments
46
+
47
+ ## Purpose
48
+ `html_comment_spans` and `strip_html_comments` answer the same question,
49
+ "which comments", and differ only in what they do with the answer. The scan,
50
+ the damage check and the exceptions are decided here, once.
51
+
52
+ ## HTML Comments Only
53
+ The template comments of the scan are the business of the template step, so
54
+ this step leaves them as they are, even when an engine is given (the engine is
55
+ needed only to read the page correctly).
56
+
57
+ ## A Damaged Page
58
+ No comments are reported at all, so a step that cuts them cuts nothing and
59
+ returns the text unchanged.
60
+ """
@@ -0,0 +1,48 @@
1
+ from simplibs.text import Span
2
+ # Inners
3
+ from .HtmlScan import HtmlScan
4
+
5
+
6
+ def find_protected_spans(scan: HtmlScan) -> list[Span]:
7
+ """Collect the ranges of a page where the whitespace steps must not change anything.
8
+
9
+ Args:
10
+ scan: The result of `scan_html`.
11
+
12
+ Returns:
13
+ The content of `pre` and `textarea`, the content of every `script` and `style` with content, and the
14
+ verbatim blocks of a template.
15
+ """
16
+
17
+ # 1. Places where a space is shown to the reader
18
+ spans = list(scan.preserved)
19
+
20
+ # 2. Code of another language: its own steps decide what to do with its whitespace
21
+ spans.extend(block.span for block in scan.blocks)
22
+
23
+ # 3. Text that a template engine passes on as it is
24
+ spans.extend(scan.verbatim)
25
+
26
+ return spans
27
+
28
+
29
+ _DESIGN_NOTES = """
30
+ # find_protected_spans — What the Whitespace Steps Leave Alone
31
+
32
+ ## Purpose
33
+ `cleanup_html_whitespace` and `compact_html` tidy the lines of a page. These
34
+ ranges are the exceptions.
35
+
36
+ ## `pre` and `textarea`
37
+ A browser shows the spaces and the line breaks inside them as they are.
38
+
39
+ ## `script` and `style`
40
+ Whether the blank lines of a script may go is a question for the steps of the
41
+ language of the script (`apply_to_html_blocks` gives them the content). A line
42
+ break inside a template string of JavaScript is part of the string, and the
43
+ whitespace steps of HTML cannot know where one is.
44
+
45
+ ## Verbatim Blocks
46
+ `{% verbatim %}` and `{% raw %}` pass their content to the output as it is; it
47
+ is often a piece of code shown in a manual.
48
+ """
@@ -0,0 +1,37 @@
1
+ from simplibs.text import Span
2
+ # Outers
3
+ from ...annotations import TemplateEngine
4
+ from ...constants import HTML_TEMPLATE_IMPORT_TAGS
5
+ # Inners
6
+ from .TemplateScan import TemplateScan
7
+ from .fit_removal_span import fit_removal_span
8
+
9
+
10
+ def find_template_import_spans(text: str, scan: TemplateScan, engine: TemplateEngine) -> list[Span]:
11
+ """Find the tags that bring names into a template.
12
+
13
+ Args:
14
+ text: The source.
15
+ scan: The result of `scan_templates` for `text`.
16
+ engine: `"django"` or `"jinja"`.
17
+
18
+ Returns:
19
+ A span for each import tag, from the blanks in front of it to its end.
20
+ """
21
+
22
+ # 1. The names of the engine's imports, and every recorded tag that has one of them
23
+ names = HTML_TEMPLATE_IMPORT_TAGS[engine]
24
+ return [fit_removal_span(text, *tag.span) for tag in scan.tags if tag.name in names]
25
+
26
+
27
+ _DESIGN_NOTES = """
28
+ # find_template_import_spans — The Imports of a Template
29
+
30
+ ## Purpose
31
+ `remove_template_imports` cuts these ranges.
32
+
33
+ ## Blanks Go With the Tag
34
+ Like a comment, an import tag takes the blanks in front of it when something
35
+ blank follows, so `{% load a %} {% load b %}` leaves no stray space and a tag
36
+ alone on its line leaves no empty line.
37
+ """
@@ -0,0 +1,40 @@
1
+ from simplibs.text import Span
2
+ # Inners
3
+ from .find_blanks_start import find_blanks_start
4
+
5
+
6
+ def fit_removal_span(text: str, start: int, end: int) -> Span:
7
+ """Decide what to cut for a comment: the comment, and the blanks in front of it when they belong to it.
8
+
9
+ Args:
10
+ text: The source.
11
+ start: The offset where the comment begins.
12
+ end: The offset just after the comment (or the end of the text).
13
+
14
+ Returns:
15
+ The span to cut.
16
+ """
17
+
18
+ # 1. What stands behind the comment; the end of the text counts as a line break
19
+ after = text[end] if end < len(text) else "\n"
20
+
21
+ # 2. A comment followed by a blank or a line break takes the blanks in front of it along
22
+ if after in " \t\r\n":
23
+ return find_blanks_start(text, start), end
24
+
25
+ # 3. Otherwise only the comment goes
26
+ return start, end
27
+
28
+
29
+ _DESIGN_NOTES = """
30
+ # fit_removal_span — The Cut of One Comment
31
+
32
+ ## Purpose
33
+ `<b>a</b> <!-- c -->` loses its trailing blank together with the comment, and
34
+ `<b>a</b><!-- c --><i>b</i>` loses nothing but the comment.
35
+
36
+ ## No Gap Needed
37
+ A comment between two words (`a<!-- c -->b`) is two pieces of text that a
38
+ browser shows as one, without a space. Cutting the comment out changes
39
+ nothing, so unlike in stylesheets no space has to be left behind.
40
+ """
@@ -0,0 +1,26 @@
1
+ from ...constants import HTML_LICENSE_START, HTML_LICENSE_TAGS
2
+
3
+
4
+ def is_license_comment(comment: str) -> bool:
5
+ """Say whether a comment is one that tools keep (a licence, a banner).
6
+
7
+ Args:
8
+ comment: The text of the comment, with its `<!--` and `-->` (blanks in front are allowed).
9
+
10
+ Returns:
11
+ `True` if it begins with `<!--!` or contains `@license` or `@preserve`.
12
+ """
13
+
14
+ # 1. The marker at the start, or one of the words anywhere
15
+ stripped = comment.lstrip()
16
+ return stripped.startswith(HTML_LICENSE_START) or any(tag in stripped for tag in HTML_LICENSE_TAGS)
17
+
18
+
19
+ _DESIGN_NOTES = """
20
+ # is_license_comment — Who Wants to Be Kept
21
+
22
+ ## Purpose
23
+ The only comments a reader of a page may not want to lose are the licence and
24
+ the banner of a template. The convention that marks them is the one
25
+ minifiers already follow.
26
+ """
@@ -0,0 +1,23 @@
1
+ def line_of(text: str, offset: int) -> int:
2
+ """Say on which line an offset lies.
3
+
4
+ Args:
5
+ text: The source.
6
+ offset: A position in it.
7
+
8
+ Returns:
9
+ The line number, counted from 1.
10
+ """
11
+
12
+ # 1. One more than the number of line breaks in front of the offset
13
+ return text.count("\n", 0, offset) + 1
14
+
15
+
16
+ _DESIGN_NOTES = """
17
+ # line_of — Where Was That
18
+
19
+ ## Purpose
20
+ A warning that says "a comment is not closed" is useless without the line.
21
+ The count is made only when there is a problem to report, so a healthy file
22
+ never pays for it.
23
+ """
@@ -0,0 +1,38 @@
1
+ from typing import Any, NoReturn
2
+ from simplibs.exception.exceptions import ParamError
3
+
4
+
5
+ def raise_block_result_invalid(kind: str, result: Any) -> NoReturn:
6
+ """Report a step for a `script` or `style` block that did not return a text.
7
+
8
+ Args:
9
+ kind: `"script"` or `"style"`.
10
+ result: What the step returned.
11
+
12
+ Raises:
13
+ ParamError: Always.
14
+ """
15
+
16
+ # 1. Name the step, say what was expected and what came back
17
+ raise ParamError(
18
+ label=f"the step for a {kind} block",
19
+ expected="a function that takes the text of the block and returns a str",
20
+ value=result,
21
+ problem="it returned something that is not a str",
22
+ how_to_fix="Give a step that returns the new text of the block (an action of simplibs-text does).",
23
+ )
24
+
25
+
26
+ _DESIGN_NOTES = """
27
+ # raise_block_result_invalid — The Step Gave Back the Wrong Thing
28
+
29
+ ## Purpose
30
+ The steps of a block are given by the caller: a lambda, a function or a whole
31
+ pipeline. The text would otherwise fail deep in a `join` with a message about
32
+ `str` and `NoneType`, far from the cause. The error names the block and the
33
+ step.
34
+
35
+ ## Why Not a Check of the Argument
36
+ Every function with the right signature is a step for this one; what it
37
+ returns is known only when it is called.
38
+ """
@@ -0,0 +1,42 @@
1
+ from collections.abc import Sequence
2
+ from simplibs.text import Span
3
+
4
+
5
+ def replace_spans(text: str, replacements: Sequence[tuple[Span, str]]) -> str:
6
+ """Put a new text in place of each of some ranges.
7
+
8
+ Args:
9
+ text: The source.
10
+ replacements: Pairs of a range and its new text, in the order of the text, without overlaps.
11
+
12
+ Returns:
13
+ The text with the ranges replaced; `text` itself when there is nothing to replace.
14
+ """
15
+
16
+ # 1. Nothing to do
17
+ if not replacements:
18
+ return text
19
+
20
+ # 2. What lies between the ranges is kept, each range gives way to its new text
21
+ pieces: list[str] = []
22
+ previous = 0
23
+ for (start, end), new_text in replacements:
24
+ pieces.append(text[previous:start])
25
+ pieces.append(new_text)
26
+ previous = end
27
+ pieces.append(text[previous:])
28
+
29
+ return "".join(pieces)
30
+
31
+
32
+ _DESIGN_NOTES = """
33
+ # replace_spans — Put Something Else in the Same Place
34
+
35
+ ## Purpose
36
+ `remove_spans` of `simplibs-text` only removes. The block step has to put the
37
+ cleaned content of every `script` and `style` where the original was.
38
+
39
+ ## Order
40
+ The ranges come in the order of the page, as the scanner finds them, so a
41
+ single pass is enough.
42
+ """
@@ -0,0 +1,45 @@
1
+ # Outers
2
+ from ...annotations import TemplateEngine
3
+ # Inners
4
+ from .HtmlScan import HtmlScan
5
+ from .scan_html import scan_html
6
+ from .warn_problems import warn_problems
7
+
8
+
9
+ def scan_checked(text: str, *, engine: TemplateEngine | None, collect_tags: bool = False) -> HtmlScan | None:
10
+ """Scan a page and say so when it cannot be trusted.
11
+
12
+ Args:
13
+ text: The source.
14
+ engine: The template language of the page; `None` for a page that is only HTML.
15
+ collect_tags: Also record every start tag and end tag (for the outline).
16
+
17
+ Returns:
18
+ The scan; `None` if the scanner noticed damage (a warning has been given for each problem).
19
+ """
20
+
21
+ # 1. One pass
22
+ scan = scan_html(text, engine=engine, collect_tags=collect_tags)
23
+
24
+ # 2. A page with damage is not read any further: the step leaves it as it is
25
+ if scan.problems:
26
+ warn_problems(scan.problems)
27
+ return None
28
+
29
+ return scan
30
+
31
+
32
+ _DESIGN_NOTES = """
33
+ # scan_checked — Trust the Scan Only if It Found Nothing Wrong
34
+
35
+ ## Purpose
36
+ Every step starts the same way: scan, and if the page looks damaged, give up
37
+ on it. The extractor's contract for such a file is that the step returns the
38
+ text unchanged and warns; the file then stays in the output as `partial`.
39
+ This is the one place where that is decided for the whole library.
40
+
41
+ ## Why Not Go On
42
+ A comment that is never closed would swallow the rest of the page, and a tag
43
+ that is never closed would hide whatever follows it. Cutting on such a basis
44
+ loses text silently; leaving the page alone loses nothing.
45
+ """