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,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
|
+
"""
|