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.
- simplibs/extractor_html/__init__.py +82 -0
- simplibs/extractor_html/actions/ApplyToHtmlBlocks.py +29 -0
- simplibs/extractor_html/actions/CleanupHtmlWhitespace.py +22 -0
- simplibs/extractor_html/actions/CompactHtml.py +22 -0
- simplibs/extractor_html/actions/HtmlOutline.py +23 -0
- simplibs/extractor_html/actions/RemoveTemplateImports.py +22 -0
- simplibs/extractor_html/actions/StripHtmlComments.py +23 -0
- simplibs/extractor_html/actions/StripTemplateComments.py +27 -0
- simplibs/extractor_html/actions/TemplateOutline.py +23 -0
- simplibs/extractor_html/actions/__init__.py +57 -0
- simplibs/extractor_html/annotations/TemplateEngine.py +26 -0
- simplibs/extractor_html/annotations/__init__.py +14 -0
- simplibs/extractor_html/constants/HTML_ANY_TAG_PATTERN.py +16 -0
- simplibs/extractor_html/constants/HTML_BLOCK_LANGUAGE_BY_TYPE.py +41 -0
- simplibs/extractor_html/constants/HTML_COMMENT_END_PATTERN.py +19 -0
- simplibs/extractor_html/constants/HTML_CONDITIONAL_PATTERN.py +23 -0
- simplibs/extractor_html/constants/HTML_DEPENDENCY_DIR_NAMES.py +18 -0
- simplibs/extractor_html/constants/HTML_EXTRA_LANGUAGE_BY_SUFFIX.py +31 -0
- simplibs/extractor_html/constants/HTML_HEADING_ELEMENTS.py +15 -0
- simplibs/extractor_html/constants/HTML_ID_ATTRIBUTE_PATTERN.py +21 -0
- simplibs/extractor_html/constants/HTML_IMPLIED_END.py +34 -0
- simplibs/extractor_html/constants/HTML_LICENSE_START.py +14 -0
- simplibs/extractor_html/constants/HTML_LICENSE_TAGS.py +14 -0
- simplibs/extractor_html/constants/HTML_OUTLINE_ELEMENTS.py +32 -0
- simplibs/extractor_html/constants/HTML_OUTLINE_TEXT_LIMIT.py +15 -0
- simplibs/extractor_html/constants/HTML_OUTPUT_DIR_NAMES.py +43 -0
- simplibs/extractor_html/constants/HTML_PLAIN_SUFFIXES.py +18 -0
- simplibs/extractor_html/constants/HTML_RAW_TEXT_END_PATTERNS.py +28 -0
- simplibs/extractor_html/constants/HTML_SUFFIXES.py +22 -0
- simplibs/extractor_html/constants/HTML_TAG_NAME_PATTERN.py +16 -0
- simplibs/extractor_html/constants/HTML_TEMPLATE_BLOCKS.py +38 -0
- simplibs/extractor_html/constants/HTML_TEMPLATE_IMPORT_TAGS.py +29 -0
- simplibs/extractor_html/constants/HTML_TEMPLATE_OUTLINE_TAGS.py +35 -0
- simplibs/extractor_html/constants/HTML_TEMPLATE_SUFFIXES.py +19 -0
- simplibs/extractor_html/constants/HTML_TEMPLATE_TAG_NAME_PATTERN.py +16 -0
- simplibs/extractor_html/constants/HTML_TEMPLATE_TOKEN_PATTERN.py +16 -0
- simplibs/extractor_html/constants/HTML_TEXT_ELEMENTS.py +16 -0
- simplibs/extractor_html/constants/HTML_TOKEN_PATTERN.py +26 -0
- simplibs/extractor_html/constants/HTML_TYPE_ATTRIBUTE_PATTERN.py +21 -0
- simplibs/extractor_html/constants/HTML_VOID_ELEMENTS.py +21 -0
- simplibs/extractor_html/constants/HTML_WRAPPED_STEM_ENDING.py +14 -0
- simplibs/extractor_html/constants/HTML_WRAPPED_SUFFIXES.py +15 -0
- simplibs/extractor_html/constants/__init__.py +77 -0
- simplibs/extractor_html/file_rules/__init__.py +56 -0
- simplibs/extractor_html/file_rules/atomic/DEPENDENCY_DIRS.py +17 -0
- simplibs/extractor_html/file_rules/atomic/DOT_NAME.py +19 -0
- simplibs/extractor_html/file_rules/atomic/OUTPUT_DIRS.py +17 -0
- simplibs/extractor_html/file_rules/atomic/VCS_DIRS.py +21 -0
- simplibs/extractor_html/file_rules/atomic/WRAPPED_HTML.py +22 -0
- simplibs/extractor_html/file_rules/atomic/__init__.py +26 -0
- simplibs/extractor_html/file_rules/html/HTML_BASE.py +22 -0
- simplibs/extractor_html/file_rules/html/HTML_FILES.py +24 -0
- simplibs/extractor_html/file_rules/html/HTML_PLAIN_BASE.py +24 -0
- simplibs/extractor_html/file_rules/html/HTML_STRICT_EXCLUDE.py +21 -0
- simplibs/extractor_html/file_rules/html/__init__.py +20 -0
- simplibs/extractor_html/logic/__init__.py +44 -0
- simplibs/extractor_html/logic/_helpers/HtmlBlock.py +31 -0
- simplibs/extractor_html/logic/_helpers/HtmlOutlineBuilder.py +189 -0
- simplibs/extractor_html/logic/_helpers/HtmlScan.py +50 -0
- simplibs/extractor_html/logic/_helpers/HtmlTag.py +32 -0
- simplibs/extractor_html/logic/_helpers/Ranges.py +48 -0
- simplibs/extractor_html/logic/_helpers/TemplateScan.py +43 -0
- simplibs/extractor_html/logic/_helpers/TemplateTag.py +33 -0
- simplibs/extractor_html/logic/_helpers/__init__.py +68 -0
- simplibs/extractor_html/logic/_helpers/blank_spans.py +40 -0
- simplibs/extractor_html/logic/_helpers/build_html_outline.py +38 -0
- simplibs/extractor_html/logic/_helpers/build_template_outline.py +75 -0
- simplibs/extractor_html/logic/_helpers/find_blanks_start.py +29 -0
- simplibs/extractor_html/logic/_helpers/find_block_language.py +35 -0
- simplibs/extractor_html/logic/_helpers/find_comments.py +60 -0
- simplibs/extractor_html/logic/_helpers/find_protected_spans.py +48 -0
- simplibs/extractor_html/logic/_helpers/find_template_import_spans.py +37 -0
- simplibs/extractor_html/logic/_helpers/fit_removal_span.py +40 -0
- simplibs/extractor_html/logic/_helpers/is_license_comment.py +26 -0
- simplibs/extractor_html/logic/_helpers/line_of.py +23 -0
- simplibs/extractor_html/logic/_helpers/raise_block_result_invalid.py +38 -0
- simplibs/extractor_html/logic/_helpers/replace_spans.py +42 -0
- simplibs/extractor_html/logic/_helpers/scan_checked.py +45 -0
- simplibs/extractor_html/logic/_helpers/scan_html.py +208 -0
- simplibs/extractor_html/logic/_helpers/scan_templates.py +128 -0
- simplibs/extractor_html/logic/_helpers/scan_templates_checked.py +39 -0
- simplibs/extractor_html/logic/_helpers/skip_html_comment.py +41 -0
- simplibs/extractor_html/logic/_helpers/skip_html_tag.py +46 -0
- simplibs/extractor_html/logic/_helpers/tidy_lines.py +86 -0
- simplibs/extractor_html/logic/_helpers/warn_problems.py +32 -0
- simplibs/extractor_html/logic/apply_to_html_blocks.py +103 -0
- simplibs/extractor_html/logic/cleanup_html_whitespace.py +54 -0
- simplibs/extractor_html/logic/compact_html.py +44 -0
- simplibs/extractor_html/logic/html_comment_spans.py +54 -0
- simplibs/extractor_html/logic/html_outline.py +66 -0
- simplibs/extractor_html/logic/strip_html_comments.py +63 -0
- simplibs/extractor_html/logic/templates/__init__.py +25 -0
- simplibs/extractor_html/logic/templates/remove_template_imports.py +51 -0
- simplibs/extractor_html/logic/templates/strip_template_comments.py +60 -0
- simplibs/extractor_html/logic/templates/template_comment_spans.py +49 -0
- simplibs/extractor_html/logic/templates/template_outline.py +62 -0
- simplibs/extractor_html/plugin/__init__.py +14 -0
- simplibs/extractor_html/plugin/register_all.py +60 -0
- simplibs/extractor_html/presets/__init__.py +43 -0
- simplibs/extractor_html/presets/html_clean_steps.py +54 -0
- simplibs/extractor_html/presets/html_no_imports_steps.py +50 -0
- simplibs/extractor_html/presets/html_outline_steps.py +28 -0
- simplibs/extractor_html/presets/template_structure_steps.py +27 -0
- simplibs/extractor_html/profiles/DJANGO_CLEAN.py +36 -0
- simplibs/extractor_html/profiles/DJANGO_NO_IMPORTS.py +32 -0
- simplibs/extractor_html/profiles/DJANGO_OUTLINE.py +32 -0
- simplibs/extractor_html/profiles/DJANGO_STRUCTURE.py +32 -0
- simplibs/extractor_html/profiles/HTML_CODE_CLEAN.py +32 -0
- simplibs/extractor_html/profiles/HTML_CODE_NO_IMPORTS.py +30 -0
- simplibs/extractor_html/profiles/HTML_DATA_ALL.py +26 -0
- simplibs/extractor_html/profiles/HTML_OUTLINE.py +31 -0
- simplibs/extractor_html/profiles/HTML_PROFILES.py +42 -0
- simplibs/extractor_html/profiles/HTML_SRC_CLEAN.py +31 -0
- simplibs/extractor_html/profiles/JINJA_CLEAN.py +35 -0
- simplibs/extractor_html/profiles/JINJA_NO_IMPORTS.py +32 -0
- simplibs/extractor_html/profiles/JINJA_OUTLINE.py +30 -0
- simplibs/extractor_html/profiles/JINJA_STRUCTURE.py +31 -0
- simplibs/extractor_html/profiles/__init__.py +86 -0
- simplibs/extractor_html/profiles/steps_for_engine.py +39 -0
- simplibs/extractor_html/profiles/steps_for_pages.py +36 -0
- simplibs/extractor_html/py.typed +1 -0
- simplibs_extractor_html-0.1.0.dist-info/METADATA +504 -0
- simplibs_extractor_html-0.1.0.dist-info/RECORD +127 -0
- simplibs_extractor_html-0.1.0.dist-info/WHEEL +5 -0
- simplibs_extractor_html-0.1.0.dist-info/entry_points.txt +2 -0
- simplibs_extractor_html-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
"""
|