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,21 @@
1
+ from typing import Final
2
+ from simplibs.path import IGNORE_VCS
3
+ from simplibs.path.rules import AnyParentNameIs
4
+ from simplibs.rules import IsIn, Rule
5
+
6
+
7
+ VCS_DIRS: Final[Rule] = AnyParentNameIs(IsIn(IGNORE_VCS))
8
+ """The file lies inside a version-control folder (`.git`, `.svn`, `.hg`, `.bzr`)."""
9
+
10
+
11
+ _DESIGN_NOTES = """
12
+ # VCS_DIRS — Not the Project's Own Files
13
+
14
+ ## Purpose
15
+ Part of the strict HTML exclusion. The names are `IGNORE_VCS` of
16
+ `simplibs-path`: version control is the same for every language, so the
17
+ library borrows the list instead of keeping its own.
18
+
19
+ ## Whole Names Only
20
+ `IsIn` compares a folder name as a whole, so `.github` is not `.git`.
21
+ """
@@ -0,0 +1,22 @@
1
+ from typing import Final
2
+ from simplibs.path.rules import HasSuffix, StemIs
3
+ from simplibs.rules import EndsWith, Rule
4
+ # Outers
5
+ from ...constants import HTML_WRAPPED_STEM_ENDING, HTML_WRAPPED_SUFFIXES
6
+
7
+
8
+ WRAPPED_HTML: Final[Rule] = HasSuffix(*HTML_WRAPPED_SUFFIXES) & StemIs(EndsWith(HTML_WRAPPED_STEM_ENDING))
9
+ """The file is a page wrapped in a template suffix: `index.html.j2`."""
10
+
11
+
12
+ _DESIGN_NOTES = """
13
+ # WRAPPED_HTML — A Page Behind a `.j2`
14
+
15
+ ## Purpose
16
+ `.j2` alone could be anything, so the rule asks for both: the wrapper suffix
17
+ and a stem that ends in `.html`. `nginx.conf.j2` fails the second half.
18
+
19
+ ## Why `StemIs`
20
+ `StemIs` judges the name without its last suffix, which for `index.html.j2`
21
+ is `index.html`.
22
+ """
@@ -0,0 +1,26 @@
1
+ from .DEPENDENCY_DIRS import DEPENDENCY_DIRS
2
+ from .DOT_NAME import DOT_NAME
3
+ from .OUTPUT_DIRS import OUTPUT_DIRS
4
+ from .VCS_DIRS import VCS_DIRS
5
+ from .WRAPPED_HTML import WRAPPED_HTML
6
+
7
+
8
+ _DESIGN_NOTES = """
9
+ # atomic — The Shared Pieces of the File Rules
10
+
11
+ Plain imports, no `__all__` (the package root decides what is public).
12
+
13
+ ## Internal Components Registry
14
+
15
+ | Component | Type | Description |
16
+ | :---------------- | :--- | :----------------------------------------------------------- |
17
+ | `DEPENDENCY_DIRS` | Rule | A folder above the file holds pages of other people. |
18
+ | `DOT_NAME` | Rule | The name starts with a dot. |
19
+ | `OUTPUT_DIRS` | Rule | A folder above the file holds generated pages. |
20
+ | `VCS_DIRS` | Rule | A folder above the file is a version-control folder. |
21
+ | `WRAPPED_HTML` | Rule | The file is `something.html` with a `.j2` wrapper. |
22
+
23
+ ## Layering & Design
24
+ - Every piece is one rule of one idea; the `html` folder only combines them.
25
+ - The rules about folders use `AnyParentNameIs` of `simplibs-path` and `IsIn` of `simplibs-rules`.
26
+ """
@@ -0,0 +1,22 @@
1
+ from typing import Final
2
+ from simplibs.rules import Rule
3
+ # Outers
4
+ from ..atomic import DOT_NAME
5
+ # Inners
6
+ from .HTML_FILES import HTML_FILES
7
+
8
+
9
+ HTML_BASE: Final[Rule] = HTML_FILES & ~DOT_NAME
10
+ """The default rule for pages worth reading: every page and template that is not hidden."""
11
+
12
+
13
+ _DESIGN_NOTES = """
14
+ # HTML_BASE — The Default Choice of Pages
15
+
16
+ ## Purpose
17
+ Every HTML profile of this library uses it as `include`.
18
+
19
+ ## No Switches
20
+ Nothing in a page needs a switch: a partial such as `_header.html` or
21
+ `base.html` is a perfectly ordinary source file and is always taken.
22
+ """
@@ -0,0 +1,24 @@
1
+ from typing import Final
2
+ from simplibs.path.rules import HasSuffix
3
+ from simplibs.rules import Rule
4
+ # Outers
5
+ from ...constants import HTML_PLAIN_SUFFIXES, HTML_TEMPLATE_SUFFIXES
6
+ from ..atomic import WRAPPED_HTML
7
+
8
+
9
+ HTML_FILES: Final[Rule] = HasSuffix(*HTML_PLAIN_SUFFIXES, *HTML_TEMPLATE_SUFFIXES) | WRAPPED_HTML
10
+ """The file is an HTML page or template (`.html .htm .xhtml .jinja .jinja2 .njk`, or `.html.j2`)."""
11
+
12
+
13
+ _DESIGN_NOTES = """
14
+ # HTML_FILES — Pages and Templates
15
+
16
+ ## Purpose
17
+ The core of `HTML_BASE`. A suffix decides for the first two groups; the
18
+ `.j2` wrapper needs the look behind it that `WRAPPED_HTML` makes.
19
+
20
+ ## Django Templates
21
+ They are `.html` files and are taken with the pages. Which steps a file gets
22
+ cannot depend on whether it holds template tags; the steps that know them
23
+ leave a plain page alone.
24
+ """
@@ -0,0 +1,24 @@
1
+ from typing import Final
2
+ from simplibs.path.rules import HasSuffix
3
+ from simplibs.rules import Rule
4
+ # Outers
5
+ from ...constants import HTML_PLAIN_SUFFIXES
6
+ from ..atomic import DOT_NAME
7
+
8
+
9
+ HTML_PLAIN_BASE: Final[Rule] = HasSuffix(*HTML_PLAIN_SUFFIXES) & ~DOT_NAME
10
+ """The pages whose suffix is `.html`, `.htm` or `.xhtml`, hidden ones left out: the files a Django project keeps its templates in."""
11
+
12
+
13
+ _DESIGN_NOTES = """
14
+ # HTML_PLAIN_BASE — Pages Without the Jinja Suffixes
15
+
16
+ ## Purpose
17
+ The `include` of the Django profiles. A Django template is a `.html` file, and
18
+ a file with a suffix of the Jinja family (`.jinja`, `.j2`) in a Django project
19
+ is not read by Django's engine, so it is not read as a Django template here
20
+ either.
21
+
22
+ ## Not `HTML_BASE`
23
+ `HTML_BASE` also takes the Jinja suffixes, which the Jinja profiles need.
24
+ """
@@ -0,0 +1,21 @@
1
+ from typing import Final
2
+ from simplibs.rules import Rule
3
+ # Outers
4
+ from ..atomic import DEPENDENCY_DIRS, OUTPUT_DIRS, VCS_DIRS
5
+
6
+
7
+ HTML_STRICT_EXCLUDE: Final[Rule] = VCS_DIRS | DEPENDENCY_DIRS | OUTPUT_DIRS
8
+ """Everything that is a page but not the project's own source: packages of other people, generated output, version control."""
9
+
10
+
11
+ _DESIGN_NOTES = """
12
+ # HTML_STRICT_EXCLUDE — Only the Project's Own Pages
13
+
14
+ ## Purpose
15
+ The `exclude` of the profile `html/src-clean`.
16
+
17
+ ## What Is Not Excluded
18
+ There are no tests, no configuration files and no asset folders in this
19
+ rule: a page is not tested, no tool keeps its settings in one, and
20
+ `templates/` and `static/` are where a Django project keeps its own files.
21
+ """
@@ -0,0 +1,20 @@
1
+ from .HTML_BASE import HTML_BASE
2
+ from .HTML_FILES import HTML_FILES
3
+ from .HTML_PLAIN_BASE import HTML_PLAIN_BASE
4
+ from .HTML_STRICT_EXCLUDE import HTML_STRICT_EXCLUDE
5
+
6
+
7
+ _DESIGN_NOTES = """
8
+ # html — File Rules for Pages and Templates
9
+
10
+ Plain imports, no `__all__` (the package root decides what is public).
11
+
12
+ ## Internal Components Registry
13
+
14
+ | Component | Type | Description |
15
+ | :-------------------- | :--- | :------------------------------------------------------------------ |
16
+ | `HTML_BASE` | Rule | The default choice of pages: every one that is not hidden. |
17
+ | `HTML_FILES` | Rule | Files with a page or template suffix. |
18
+ | `HTML_PLAIN_BASE` | Rule | The pages with a `.html`, `.htm` or `.xhtml` suffix, not hidden. |
19
+ | `HTML_STRICT_EXCLUDE` | Rule | Other people's packages, generated output, version control. |
20
+ """
@@ -0,0 +1,44 @@
1
+ from .apply_to_html_blocks import apply_to_html_blocks
2
+ from .cleanup_html_whitespace import cleanup_html_whitespace
3
+ from .compact_html import compact_html
4
+ from .html_comment_spans import html_comment_spans
5
+ from .html_outline import html_outline
6
+ from .strip_html_comments import strip_html_comments
7
+ from .templates import (
8
+ remove_template_imports,
9
+ strip_template_comments,
10
+ template_comment_spans,
11
+ template_outline,
12
+ )
13
+
14
+
15
+ _DESIGN_NOTES = """
16
+ # logic — The Steps That Read Pages
17
+
18
+ Plain imports, no `__all__` (the package root decides what is public).
19
+
20
+ ## Internal Components Registry
21
+
22
+ | Component | Type | Description |
23
+ | :------------------------- | :------- | :------------------------------------------------------------------- |
24
+ | `apply_to_html_blocks` | Function | Runs the given steps on the code inside `script` and `style`. |
25
+ | `cleanup_html_whitespace` | Function | Trims line ends, reduces blank lines to one; spares `pre` and the like. |
26
+ | `compact_html` | Function | Drops every blank line; spares `pre` and the like. |
27
+ | `html_comment_spans` | Function | The spans of the HTML comments (by the scanner). |
28
+ | `html_outline` | Function | The landmarks, headings and ids of a page, indented by nesting. |
29
+ | `remove_template_imports` | Function | Removes `{% load %}`, `{% import %}` and `{% from %}`. |
30
+ | `strip_html_comments` | Function | Removes the HTML comments. |
31
+ | `strip_template_comments` | Function | Removes the template comments. |
32
+ | `template_comment_spans` | Function | The spans of the template comments. |
33
+ | `template_outline` | Function | The skeleton of a template: extends, include, blocks. |
34
+ | `templates` | Package | The steps for Django and Jinja templates. |
35
+ | `_helpers` | Private | The scanners and the parts they are made of. |
36
+
37
+ ## Layering & Design
38
+ - A page is read by a scanner that knows tags, quoted values and raw text; it is not a parser and cannot fail.
39
+ - A template is read first, by its own scanner, and the page is then read with the template constructs blanked.
40
+ - A file that looks damaged is left as it is and reported by a warning, so the extractor calls it `partial`.
41
+ - Each step is a function of a text and returns a text; the `Action` wrappers come in `actions`.
42
+ - The steps import only `constants` and `annotations`; the ranges they find are `simplibs.text.Span`.
43
+ - The steps for the code inside a page are given as parameters: the library does not depend on the JavaScript or CSS add-on.
44
+ """
@@ -0,0 +1,31 @@
1
+ from dataclasses import dataclass
2
+ from simplibs.text import Span
3
+
4
+
5
+ @dataclass(frozen=True, slots=True)
6
+ class HtmlBlock:
7
+ """The content of a `script` or `style` element.
8
+
9
+ Attributes:
10
+ kind: `"script"` or `"style"`.
11
+ span: The range of the content, between the end of the start tag and the beginning of the end tag.
12
+ language: `"javascript"` or `"css"` when the `type` says the content is code, otherwise `None`
13
+ (JSON, a framework template, a shader...).
14
+ """
15
+
16
+ kind: str
17
+ span: Span
18
+ language: str | None
19
+
20
+
21
+ _DESIGN_NOTES = """
22
+ # HtmlBlock — A Piece of Another Language Inside a Page
23
+
24
+ ## Purpose
25
+ The block step of the next batch cleans the JavaScript in `<script>` and the
26
+ CSS in `<style>` with the steps of the other add-ons. It needs to know where
27
+ the content is and whether it is code at all.
28
+
29
+ ## Only Non-Blank Content
30
+ An element with nothing in it (`<script src="a.js"></script>`) is not a block.
31
+ """
@@ -0,0 +1,189 @@
1
+ from dataclasses import dataclass
2
+ from simplibs.text import Span
3
+ # Outers
4
+ from ...constants import (
5
+ HTML_ANY_TAG_PATTERN,
6
+ HTML_HEADING_ELEMENTS,
7
+ HTML_ID_ATTRIBUTE_PATTERN,
8
+ HTML_IMPLIED_END,
9
+ HTML_OUTLINE_ELEMENTS,
10
+ HTML_OUTLINE_TEXT_LIMIT,
11
+ HTML_TEXT_ELEMENTS,
12
+ HTML_VOID_ELEMENTS,
13
+ )
14
+
15
+
16
+ @dataclass(slots=True)
17
+ class _Open:
18
+ """An element that has been opened and not closed yet."""
19
+
20
+ name: str
21
+ listed: bool
22
+ line: int | None
23
+ content_start: int
24
+
25
+
26
+ class HtmlOutlineBuilder:
27
+ """Follows the start tags and end tags of a page and writes the outline.
28
+
29
+ Args:
30
+ text: The source.
31
+ max_depth: The number of levels to list, or `None` for all of them.
32
+ """
33
+
34
+ __slots__ = ("_text", "_max_depth", "_stack", "_depth", "_lines")
35
+
36
+ def __init__(self, text: str, max_depth: int | None) -> None:
37
+ self._text = text
38
+ self._max_depth = max_depth
39
+ self._stack: list[_Open] = []
40
+ self._depth = 0
41
+ self._lines: list[list] = []
42
+
43
+ def start_tag(self, name: str, span: Span) -> None:
44
+ """A start tag: close what it implicitly closes, list the element if it belongs in the outline, open it."""
45
+
46
+ # 1. An end tag that may be left out is implied by the start tag that follows
47
+ self._close_implied(name)
48
+
49
+ # 2. Is the element listed, and under what name
50
+ raw = self._text[span[0]:span[1]]
51
+ element_id = self._find_id(raw)
52
+ listed = name in HTML_OUTLINE_ELEMENTS or name in HTML_TEXT_ELEMENTS or element_id is not None
53
+
54
+ # 3. A listed element writes its line now, unless it is deeper than wanted
55
+ line: int | None = None
56
+ if listed and (self._max_depth is None or self._depth < self._max_depth):
57
+ label = name if element_id is None else f"{name}#{element_id}"
58
+ self._lines.append([self._depth, label, ""])
59
+ line = len(self._lines) - 1
60
+
61
+ # 4. An element that is complete in its tag opens nothing
62
+ if name in HTML_VOID_ELEMENTS or raw.endswith("/>"):
63
+ return
64
+
65
+ # 5. Anything else stays open until its end tag (or until a later tag implies it)
66
+ self._stack.append(_Open(name, listed, line, span[1]))
67
+ if listed:
68
+ self._depth += 1
69
+
70
+ def end_tag(self, name: str, span: Span) -> None:
71
+ """An end tag: close the nearest open element of that name, and everything opened inside it."""
72
+
73
+ # 1. The nearest open element with this name; an end tag with none is ignored
74
+ for index in range(len(self._stack) - 1, -1, -1):
75
+ if self._stack[index].name == name:
76
+ self._pop_to(index, closing_start=span[0])
77
+ return
78
+
79
+ def lines(self) -> list[str]:
80
+ """The finished lines of the outline."""
81
+
82
+ # 1. Indented by depth; the text of a heading in quotes behind its label
83
+ written = []
84
+ for depth, label, text in self._lines:
85
+ written.append(" " * depth + label + (f' "{text}"' if text else ""))
86
+
87
+ return written
88
+
89
+ def _close_implied(self, name: str) -> None:
90
+ """Pop the open elements that a start tag of this name closes by itself."""
91
+
92
+ # 1. A heading directly inside a heading ends it: `<h1>a<h2>b` has two headings side by side
93
+ if name in HTML_HEADING_ELEMENTS and self._stack and self._stack[-1].name in HTML_HEADING_ELEMENTS:
94
+ self._pop_to(len(self._stack) - 1, closing_start=None)
95
+ return
96
+
97
+ # 2. Only elements whose end tag may be left out have a rule
98
+ rule = HTML_IMPLIED_END.get(name)
99
+ if rule is None:
100
+ return
101
+ closes, bounds = rule
102
+
103
+ # 3. From the top down: a bound stops the search, the outermost element that is closed is the target
104
+ target: int | None = None
105
+ for index in range(len(self._stack) - 1, -1, -1):
106
+ open_name = self._stack[index].name
107
+ if open_name in bounds:
108
+ break
109
+ if open_name in closes:
110
+ target = index
111
+
112
+ if target is not None:
113
+ self._pop_to(target, closing_start=None)
114
+
115
+ def _pop_to(self, index: int, *, closing_start: int | None) -> None:
116
+ """Pop the open elements down to and including the one at `index`; it is the one that an end tag closes."""
117
+
118
+ # 1. The elements above it were never closed; their text, if any, is unknown
119
+ while len(self._stack) > index:
120
+ entry = self._stack.pop()
121
+ if entry.listed:
122
+ self._depth -= 1
123
+
124
+ # 2. The element itself gets the text between its tags, when it is a title or a heading
125
+ is_closed_here = len(self._stack) == index and closing_start is not None
126
+ if is_closed_here and entry.line is not None and entry.name in HTML_TEXT_ELEMENTS:
127
+ self._lines[entry.line][2] = self._clean_text(self._text[entry.content_start:closing_start])
128
+
129
+ @staticmethod
130
+ def _find_id(raw: str) -> str | None:
131
+ """The value of the `id` of a tag, with its blanks collapsed; `None` if there is none."""
132
+
133
+ # 1. The first of the three groups that matched
134
+ match = HTML_ID_ATTRIBUTE_PATTERN.search(raw)
135
+ if match is None:
136
+ return None
137
+ value = " ".join(next(group for group in match.groups() if group is not None).split())
138
+
139
+ return value or None
140
+
141
+ @staticmethod
142
+ def _clean_text(raw: str) -> str:
143
+ """The text of a heading: no tags, blanks collapsed, cut at the limit."""
144
+
145
+ # 1. The tags inside it go, and the blanks between words become single spaces
146
+ text = " ".join(HTML_ANY_TAG_PATTERN.sub("", raw).split())
147
+
148
+ # 2. A text that is too long is cut and marked
149
+ if len(text) > HTML_OUTLINE_TEXT_LIMIT:
150
+ return text[:HTML_OUTLINE_TEXT_LIMIT - 1].rstrip() + "…"
151
+
152
+ return text
153
+
154
+
155
+ _DESIGN_NOTES = """
156
+ # HtmlOutlineBuilder — A Little Nesting, Just Enough for the Outline
157
+
158
+ ## Purpose
159
+ The outline shows which parts of a page lie inside which. That needs a stack of
160
+ the elements that are open, not a tree of the whole page, and the builder is
161
+ that stack with the lines it has written.
162
+
163
+ ## What It Lists
164
+ An element is listed when it is one of `HTML_OUTLINE_ELEMENTS`, a title or a
165
+ heading, or has an `id`. Its depth is the number of listed elements around it,
166
+ so a `div` in a `div` in a `section` costs no indentation.
167
+
168
+ ## End Tags That May Be Left Out
169
+ `<li>a<li>b` and `<td>a<td>b` are valid. A start tag with an entry in
170
+ `HTML_IMPLIED_END` closes the open elements it implies, up to the bound of its
171
+ entry.
172
+
173
+ ## Headings
174
+ A heading that follows a heading directly closes it (`<h1>a<h2>b`), because a
175
+ heading cannot contain another.
176
+
177
+ ## The Text of a Heading
178
+ It is what lies between the start tag and the end tag, without tags, with blanks
179
+ collapsed. A heading that is closed only implicitly has no text: the builder
180
+ does not guess where it ended.
181
+
182
+ ## Stray End Tags
183
+ An end tag with no open element of that name is ignored, as a browser does.
184
+
185
+ ## Known Limits
186
+ `p` and the other elements whose end tag a following element implies are not
187
+ followed. A stray `</div>` closes the nearest open `div`, which may not be the
188
+ one the author meant.
189
+ """
@@ -0,0 +1,50 @@
1
+ from dataclasses import dataclass
2
+ from simplibs.text import Span
3
+ # Inners
4
+ from .HtmlBlock import HtmlBlock
5
+ from .HtmlTag import HtmlTag
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class HtmlScan:
10
+ """What the scanner found in a page.
11
+
12
+ Attributes:
13
+ comments: The spans of every HTML comment, with the blanks in front of it where they can go.
14
+ conditional: The comments (a subset of `comments`) that are Internet Explorer conditional comments.
15
+ template_comments: The spans of the template comments (`{# ... #}`, `{% comment %}`); empty
16
+ when no engine was given.
17
+ verbatim: The spans of the template blocks that are plain text (`{% verbatim %}`, `{% raw %}`).
18
+ blocks: The `script` and `style` elements with content.
19
+ preserved: The spans where whitespace has a meaning: the content of `pre` and `textarea`.
20
+ tags: Every start tag and end tag, in the order of the page; empty unless they were asked for.
21
+ problems: What looked damaged, one sentence each.
22
+ """
23
+
24
+ comments: tuple[Span, ...]
25
+ conditional: tuple[Span, ...]
26
+ template_comments: tuple[Span, ...]
27
+ verbatim: tuple[Span, ...]
28
+ blocks: tuple[HtmlBlock, ...]
29
+ preserved: tuple[Span, ...]
30
+ tags: tuple[HtmlTag, ...]
31
+ problems: tuple[str, ...]
32
+
33
+
34
+ _DESIGN_NOTES = """
35
+ # HtmlScan — The Result of One Pass
36
+
37
+ ## Purpose
38
+ A page is read once and the answers to all the questions of the steps come
39
+ together: where the comments are, which of them IE needs, where the template
40
+ comments and the raw template blocks are, which pieces are code of another
41
+ language, where whitespace matters and, when asked, which tags there are.
42
+
43
+ ## Why a Frozen Record
44
+ The lists belong together and are not changed after the scan.
45
+
46
+ ## `problems`
47
+ The scanner never refuses a text. What it notices on the way (a comment, a tag
48
+ or a `script` that is never closed) it writes down, and the steps turn it into
49
+ a warning, so the extractor can call the file `partial`.
50
+ """
@@ -0,0 +1,32 @@
1
+ from dataclasses import dataclass
2
+ from simplibs.text import Span
3
+
4
+
5
+ @dataclass(frozen=True, slots=True)
6
+ class HtmlTag:
7
+ """One start tag or end tag of a page.
8
+
9
+ Attributes:
10
+ name: The name of the element, in lower case (`div`).
11
+ closing: `True` for an end tag (`</div>`), `False` for a start tag.
12
+ span: The range of the whole tag, from `<` to `>`.
13
+ """
14
+
15
+ name: str
16
+ closing: bool
17
+ span: Span
18
+
19
+
20
+ _DESIGN_NOTES = """
21
+ # HtmlTag — A Tag, Seen From Outside
22
+
23
+ ## Purpose
24
+ The outline follows the nesting of the elements, and for that it needs the tags
25
+ in the order of the page: what each is called, whether it opens or closes and
26
+ where it is. Its attributes are read from the text of the tag when they are
27
+ wanted.
28
+
29
+ ## Recorded Only on Request
30
+ Most steps never look at a tag, and a page has a lot of them; the scanner
31
+ records them only when it is asked to (`collect_tags`).
32
+ """
@@ -0,0 +1,48 @@
1
+ from bisect import bisect_left
2
+ from collections.abc import Iterable
3
+ from simplibs.text import Span
4
+
5
+
6
+ class Ranges:
7
+ """A set of ranges of a text that can be asked whether a stretch of the text touches one of them.
8
+
9
+ Args:
10
+ spans: The ranges; they may overlap and may come in any order.
11
+ """
12
+
13
+ __slots__ = ("_spans", "_starts")
14
+
15
+ def __init__(self, spans: Iterable[Span]) -> None:
16
+
17
+ # 1. Sorted, and ranges that overlap or touch merged into one, so a question costs a binary search
18
+ merged: list[Span] = []
19
+ for start, end in sorted(spans):
20
+ if merged and start <= merged[-1][1]:
21
+ merged[-1] = (merged[-1][0], max(end, merged[-1][1]))
22
+ else:
23
+ merged.append((start, end))
24
+ self._spans = merged
25
+ self._starts = [start for start, _ in merged]
26
+
27
+ def touches(self, start: int, end: int) -> bool:
28
+ """Say whether the stretch `start:end` of the text has something in common with one of the ranges."""
29
+
30
+ # 1. The last range that begins before the stretch ends has the largest end of all that can reach it
31
+ index = bisect_left(self._starts, end) - 1
32
+ return index >= 0 and self._spans[index][1] > start
33
+
34
+
35
+ _DESIGN_NOTES = """
36
+ # Ranges — Where Whitespace Must Not Be Touched
37
+
38
+ ## Purpose
39
+ The whitespace steps work line by line and must leave alone every line that
40
+ has a part in a range where whitespace has a meaning (the content of `pre`
41
+ and `textarea`, a `script`, a `style`, a verbatim block). They ask this object
42
+ once for each line.
43
+
44
+ ## Why Merged
45
+ A `{% verbatim %}` inside a `<pre>` gives two ranges, one inside the other.
46
+ Merged, the ranges are sorted and apart, so the last one that begins before a
47
+ stretch ends is the only one that can reach it.
48
+ """
@@ -0,0 +1,43 @@
1
+ from dataclasses import dataclass
2
+ from simplibs.text import Span
3
+ # Inners
4
+ from .TemplateTag import TemplateTag
5
+
6
+
7
+ @dataclass(frozen=True, slots=True)
8
+ class TemplateScan:
9
+ """What the scanner found of the template language in a page.
10
+
11
+ Attributes:
12
+ comments: The spans of the template comments, with the blanks in front of them where they can go.
13
+ verbatim: The spans of the blocks that are plain text for the template engine.
14
+ tags: The `{% ... %}` tags, in the order of the page.
15
+ masked: The text of the same length in which every template construct (variable, tag, comment,
16
+ verbatim block) is replaced by blanks, so that the page can be read as HTML.
17
+ problems: What looked damaged, one sentence each.
18
+ """
19
+
20
+ comments: tuple[Span, ...]
21
+ verbatim: tuple[Span, ...]
22
+ tags: tuple[TemplateTag, ...]
23
+ masked: str
24
+ problems: tuple[str, ...]
25
+
26
+
27
+ _DESIGN_NOTES = """
28
+ # TemplateScan — The Template Layer of a Page
29
+
30
+ ## Purpose
31
+ `{{ x }}`, `{% if a > b %}` and `{# ... #}` are not HTML, and they may hold
32
+ characters that break the reading of HTML (`>` in a condition, quotes inside
33
+ quotes). So they are read first, by their own rules, and what is left
34
+ (`masked`) is the page as a browser would see it.
35
+
36
+ ## Same Length
37
+ The masked text has the length and the lines of the original, so every
38
+ offset the HTML scan reports is valid in both.
39
+
40
+ ## What the Steps Take From It
41
+ The comment step takes `comments`, the import step takes `tags`, and the HTML
42
+ scan takes `masked`.
43
+ """
@@ -0,0 +1,33 @@
1
+ from dataclasses import dataclass
2
+ from simplibs.text import Span
3
+
4
+
5
+ @dataclass(frozen=True, slots=True)
6
+ class TemplateTag:
7
+ """One `{% ... %}` tag of a template.
8
+
9
+ Attributes:
10
+ name: The name of the tag (`block` of `{% block content %}`).
11
+ span: The range of the whole tag, from `{%` to `%}`.
12
+ arguments: What stands behind the name, without the blanks and without the whitespace
13
+ control (`content` of `{% block content %}`; empty for `{% endblock %}`).
14
+ """
15
+
16
+ name: str
17
+ span: Span
18
+ arguments: str
19
+
20
+
21
+ _DESIGN_NOTES = """
22
+ # TemplateTag — A Tag, Seen From Outside
23
+
24
+ ## Purpose
25
+ The steps that work with the structure of a template (the imports, and later
26
+ the overview of `extends`, `block` and `include`) need three things about a
27
+ tag: what it is called, where it is and what is written in it.
28
+
29
+ ## Which Tags Are Recorded
30
+ Every tag that is not the opening of a block running to an end tag
31
+ (`comment`, `verbatim`, `raw`). What stands inside such a block is not a tag
32
+ of the template: it is a comment or a text.
33
+ """