lhtml-markup 2.3.0__tar.gz → 2.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/PKG-INFO +155 -16
  2. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/README.md +153 -14
  3. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/pyproject.toml +5 -2
  4. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/__init__.py +3 -1
  5. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/cli.py +8 -1
  6. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/export_html.py +12 -3
  7. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/patterns.py +23 -7
  8. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/pipeline.py +4 -7
  9. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/process.py +99 -36
  10. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/PKG-INFO +155 -16
  11. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/requires.txt +0 -1
  12. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/test/test_lhtml.py +234 -1
  13. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/LICENSE.md +0 -0
  14. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/setup.cfg +0 -0
  15. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/__main__.py +0 -0
  16. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/code.py +0 -0
  17. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/element_extract.py +0 -0
  18. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/errors.py +0 -0
  19. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/insert_in_text.py +0 -0
  20. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/listing.py +0 -0
  21. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/tag_element.lark +0 -0
  22. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/tag_parser.py +0 -0
  23. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml/wrap_html.py +0 -0
  24. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/SOURCES.txt +0 -0
  25. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/dependency_links.txt +0 -0
  26. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/entry_points.txt +0 -0
  27. {lhtml_markup-2.3.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/top_level.txt +0 -0
@@ -1,11 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lhtml-markup
3
- Version: 2.3.0
3
+ Version: 2.4.1
4
4
  Summary: Lightweight HTML markup language — simplifies HTML authoring with shorthand syntax
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/drohmer/lhtml
7
7
  Project-URL: Repository, https://github.com/drohmer/lhtml
8
8
  Project-URL: Issues, https://github.com/drohmer/lhtml/issues
9
+ Project-URL: Changelog, https://github.com/drohmer/lhtml/blob/main/CHANGELOG.md
9
10
  Requires-Python: >=3.10
10
11
  Description-Content-Type: text/markdown
11
12
  License-File: LICENSE.md
@@ -14,18 +15,19 @@ Requires-Dist: pygments>=2.15
14
15
  Requires-Dist: pyyaml>=6.0
15
16
  Provides-Extra: dev
16
17
  Requires-Dist: pytest>=7.0; extra == "dev"
17
- Requires-Dist: ansicolors; extra == "dev"
18
18
  Dynamic: license-file
19
19
 
20
20
  # LHTML — Lightweight HTML
21
21
 
22
- [![Tests](https://github.com/drohmer/lhtml/actions/workflows/tests.yml/badge.svg?branch=feature/lhtml-v2)](https://github.com/drohmer/lhtml/actions/workflows/tests.yml)
22
+ [![Tests](https://github.com/drohmer/lhtml/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/drohmer/lhtml/actions/workflows/tests.yml)
23
23
  [![PyPI](https://img.shields.io/pypi/v/lhtml-markup)](https://pypi.org/project/lhtml-markup/)
24
24
 
25
25
  LHTML is a markup language that simplifies HTML authoring with embedded CSS styling. It is designed to be **HTML-first**: raw HTML passes through untouched, and only a few shorthand symbols (`::`, `*`, `=`, `**`, `__`) trigger conversions.
26
26
 
27
27
  LHTML is used to build static websites and presentation slides, typically combined with Jinja2 templates.
28
28
 
29
+ See the [examples](examples/) and the [changelog](CHANGELOG.md).
30
+
29
31
  ## Installation
30
32
 
31
33
  From PyPI:
@@ -58,14 +60,44 @@ Dependencies (`lark`, `pygments`, `pyyaml`) are installed automatically.
58
60
  ```bash
59
61
  lhtml input.l.html # Convert to stdout
60
62
  lhtml input.l.html -o output.html # Convert to file
61
- lhtml input.l.html -w # Wrap in full HTML document
62
- lhtml input.l.html -b # Render source line breaks as <br>
63
+ lhtml input.l.html -w # Wrap in full HTML document (--wrapAuto)
64
+ lhtml input.l.html -b # Render source line breaks as <br> (--line-breaks)
63
65
  lhtml a.l.html b.l.html # Several files: a.html, b.html next to sources
64
66
  lhtml a.l.html b.l.html -o build/ # Several files into a directory
65
67
  python -m lhtml input.l.html # Alternative invocation
68
+ lhtml --version # Show the version (also lhtml.__version__)
66
69
  ```
67
70
 
68
- Files are read and written as UTF-8 (a BOM is accepted). Errors and warnings are reported on stderr with the file name, the remaining files are still processed, and the exit code is non-zero if any file failed. An input file is never overwritten (e.g. `lhtml page.html` without `-o`). Includes are looked up in the input file's directory first, then in the current directory.
71
+ Without `-o`, `page.l.html` is written to `page.html` next to its source.
72
+
73
+ - Files are read and written as UTF-8 (a BOM is accepted).
74
+ - Errors and warnings are reported on stderr with the file name. The remaining files are still processed, and the exit code is non-zero if any file failed.
75
+ - An input file is never overwritten: `lhtml page.html` (output `page.html`) is an error, as is an output that is another input of the batch, or a symbolic or hard link to one.
76
+ - Includes are looked up in the input file's directory first, then in the current directory.
77
+
78
+ With `-w`, the result is wrapped in a minimal HTML document using the `title`, `css` and `js` of the front matter:
79
+
80
+ ```html
81
+ <!DOCTYPE html>
82
+
83
+ <html lang="en">
84
+
85
+ <head>
86
+ <meta charset="utf-8">
87
+ <meta name="viewport" content="width=device-width, initial-scale=1">
88
+ <title>My Page</title>
89
+ <link rel="stylesheet" type="text/css" href="style.css">
90
+ <script src="app.js" defer></script>
91
+ </head>
92
+
93
+ <body>
94
+ <h1>Hello</h1>
95
+
96
+
97
+ </body>
98
+
99
+ </html>
100
+ ```
69
101
 
70
102
  ### Python API
71
103
 
@@ -114,13 +146,33 @@ With classes/IDs:
114
146
  ```
115
147
  * First item
116
148
  * Second item
117
- ** Nested item A
118
- ** Nested item B
119
- *** Deep nested
149
+ ** Nested item
120
150
  * Back to top level
121
151
  ```
122
152
 
123
- Produces nested `<ul><li>` structures.
153
+ Output (a nested list is placed in its own `<li>`):
154
+ ```html
155
+ <ul>
156
+ <li>
157
+ First item
158
+ </li>
159
+ <li>
160
+ Second item
161
+ </li>
162
+ <li>
163
+ <ul>
164
+ <li>
165
+ Nested item
166
+ </li>
167
+ </ul>
168
+ </li>
169
+ <li>
170
+ Back to top level
171
+ </li>
172
+ </ul>
173
+ ```
174
+
175
+ Each `*` adds one level (`***` is level 3). A list ends at the first line that is not an item.
124
176
 
125
177
 
126
178
  ### Inline Formatting
@@ -155,7 +207,7 @@ Tag names start with a letter and may contain letters, digits, `_` and `-` (usef
155
207
 
156
208
  A closing `::` may be directly followed by punctuation or HTML (`**span::[c] x ::**`, `important ::,`). In a run of colons, `::` markers are the pairs ending the run: `::::[...]` is a closing `::` followed by `::[...]`, and `x:::nl` is `x:` followed by `::nl`. A `::` between two HTML tags (as in pre-highlighted code `<span>::</span>`) is left untouched.
157
209
 
158
- A tag that is opened but never closed, or a `::` without matching opening tag, produces an `LHTMLWarning` quoting the beginning of the tag.
210
+ A tag that is opened but never closed, or a `::` without matching opening tag, produces an `LHTMLWarning` quoting the beginning of the tag. An unmatched `::` is kept as text in the output.
159
211
 
160
212
  #### Div / Span with Styles
161
213
 
@@ -219,10 +271,29 @@ Output:
219
271
  <div style="color:blue;"> short text </div>
220
272
  ```
221
273
 
274
+ #### Explicit Closing Tags
275
+
276
+ A bare `::` closes the last opened tag. To make long or nested blocks easier to read, name the tag you close with `::name[-]` (`::[-]` closes the last one, like `::`):
277
+
278
+ ```
279
+ div::[color:red;]
280
+ Red **text**
281
+ ::div[-]
282
+ ```
283
+
284
+ Output:
285
+ ```html
286
+ <div style="color:red;">
287
+ Red <strong>text</strong>
288
+ </div>
289
+ ```
290
+
291
+ If the name does not match the last opened tag, a `LHTMLWarning` is emitted and the last opened tag is closed.
292
+
222
293
 
223
294
  ### Links
224
295
 
225
- The URL of `link::`, `img::`, `video::` and `videoplay::` is never modified (`__`, `**`, `$` are kept). Parentheses that are part of the URL are kept (`Mercury_(planet)`, `fig(1).png`), while a group starting with `.` or `#` is a class/id group.
296
+ The URL of `link::`, `img::`, `video::` and `videoplay::` is never modified (`__`, `**`, `$` are kept). Parentheses that are part of the URL are kept (`Mercury_(planet)`, `fig(1).png`), while a group starting with `.` or `#` is a class/id group. The URL may contain Jinja expressions (`img::{{ base }}/photo.jpg`, `link::{{ url_for('page') }}[Home]`), which are kept unchanged.
226
297
 
227
298
  ```
228
299
  link::https://example.com[Click here]
@@ -267,7 +338,22 @@ def hello():
267
338
  code::[-]
268
339
  ````
269
340
 
270
- `include::file` directives inside a code block insert the file as raw code. Syntax highlighting is powered by Pygments. Any language supported by Pygments can be used. An empty language (`code::[]`) renders plain text; an unknown language renders plain text with a warning.
341
+ Output (Pygments HTML inside `<div class="code">`):
342
+ ```html
343
+ <div class="code"><pre><span></span><span class="k">def</span><span class="w"> </span><span class="nf">hello</span><span class="p">():</span>
344
+ ...
345
+ </pre></div>
346
+ ```
347
+
348
+ Syntax highlighting is powered by Pygments: any language supported by Pygments can be used, case-insensitively. An empty language (`code::[]`) renders plain text; an unknown language renders plain text with a warning. The built-in `c++` language is C++ with the types of the [CGP library](https://github.com/drohmer/cgp) highlighted; other languages can be added (see [Custom Code Lexers](#custom-code-lexers)).
349
+
350
+ The colors come from a Pygments stylesheet, which you must include in your page. Generate one for the `.code` class (any [Pygments style](https://pygments.org/styles/) can replace `default`):
351
+
352
+ ```bash
353
+ pygmentize -S default -f html -a .code > code.css
354
+ ```
355
+
356
+ `include::file` directives inside a code block insert the file as raw code.
271
357
 
272
358
 
273
359
  ### Spacer
@@ -355,6 +441,50 @@ Supported metadata keys:
355
441
  | `directory_include` | list | Directories to search for includes |
356
442
 
357
443
 
444
+ ## Using LHTML with Jinja2
445
+
446
+ LHTML leaves Jinja2 untouched, so a template can be written in LHTML: convert it to HTML first, then render it with Jinja2 (`pip install jinja2`).
447
+
448
+ `blog.l.html`:
449
+ ```
450
+ = {{ page.title }}
451
+
452
+ {% for post in posts %}
453
+ div::(.post)
454
+ == link::{{ post.url }}[{{ post.title }}]
455
+ {{ post.summary }} **Read more**
456
+ ::
457
+ {% endfor %}
458
+ ```
459
+
460
+ ```python
461
+ import jinja2
462
+ import lhtml
463
+
464
+ with open('blog.l.html', encoding='utf-8') as f:
465
+ template = jinja2.Template(lhtml.run(f.read()))
466
+
467
+ html = template.render(page={'title': 'Blog'},
468
+ posts=[{'url': 'first.html', 'title': 'First post', 'summary': 'Hello.'}])
469
+ ```
470
+
471
+ `lhtml.run()` produces the Jinja2 template:
472
+ ```html
473
+ <h1>{{ page.title }}</h1>
474
+
475
+
476
+ {% for post in posts %}
477
+ <div class="post">
478
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
479
+
480
+ {{ post.summary }} <strong>Read more</strong>
481
+ </div>
482
+ {% endfor %}
483
+ ```
484
+
485
+ Jinja2 expressions can also be used in URLs (`img::{{ base }}/photo.jpg`) and in tag groups (`div::[color:{{ color }};]`).
486
+
487
+
358
488
  ## Plugin System
359
489
 
360
490
  ### Custom Tag Handlers
@@ -421,7 +551,7 @@ All keys for the `meta` dict passed to `lhtml.run()`:
421
551
  'title': 'Webpage', # Document title
422
552
  'css': [], # CSS files (string or list)
423
553
  'js': [], # JS files (string or list)
424
- 'directory_include': [], # Search paths for include::
554
+ 'directory_include': [cwd], # Search paths for include:: (default: current directory)
425
555
  'current_directory': '', # Base directory for video codec detection
426
556
  }
427
557
  ```
@@ -429,7 +559,9 @@ All keys for the `meta` dict passed to `lhtml.run()`:
429
559
 
430
560
  ## Design Principles
431
561
 
432
- - **HTML-first**: Raw HTML is never modified. Only LHTML syntax triggers conversions. In particular, the following are never transformed (not even by `include::` or `::#`): HTML tags and their attributes (URLs containing `__`, quoted values containing `>`, ...), `<script>` and `<style>` blocks (CSS `::before`, ...), HTML comments, and math (`$...$`, `$$...$$`, `\(...\)`, `\[...\]`, so that `$x**2$` reaches MathJax/KaTeX intact). Text between HTML tags is still processed.
562
+ - **HTML-first**: Raw HTML is never modified. Only LHTML syntax triggers conversions. In particular, the following are never transformed (not even by `include::` or `::#`): HTML tags and their attributes, including tags spanning multiple lines (URLs containing `__`, quoted values containing `>`, ...), `<script>` and `<style>` blocks (CSS `::before`, ...), HTML comments, and math (`$...$`, `$$...$$`, `\(...\)`, `\[...\]`, so that `$x**2$` reaches MathJax/KaTeX intact). Text between HTML tags is still processed.
563
+ - **Attributes stay literal**: styles, classes/IDs and HTML attributes in LHTML tag groups and headings are preserved without inline formatting (`div::(.my__class__)` keeps `my__class__`). Link labels still support formatting.
564
+ - **Template-friendly**: Jinja2 expressions (`{{ ... }}`), statements (`{% ... %}`) and comments (`{# ... #}`) pass through unchanged (see [Using LHTML with Jinja2](#using-lhtml-with-jinja2)).
433
565
  - **Island grammar**: LHTML syntax "islands" float in a sea of opaque content (HTML, Jinja2 templates, LaTeX, etc.) that passes through untouched.
434
566
  - **Minimal**: A few symbols (`::`, `=`, `*`, `**`, `__`, `` ` ``) cover most needs. No complex configuration required.
435
567
  - **Composable**: LHTML works seamlessly with Jinja2 templates, making it suitable for static site generators.
@@ -440,20 +572,27 @@ All keys for the `meta` dict passed to `lhtml.run()`:
440
572
  ```
441
573
  src/lhtml/
442
574
  __init__.py # Public API: run(), analyse_tag(), read_yaml()
575
+ __main__.py # python -m lhtml
443
576
  cli.py # Command-line interface
444
577
  pipeline.py # ProcessingPipeline, TagRegistry, LexerRegistry
445
578
  process.py # Core transformation functions
446
579
  patterns.py # Centralized regex patterns and utilities
447
580
  tag_parser.py # Lark-based parser for :: bracket syntax
448
581
  tag_element.lark # Lark grammar definition
582
+ element_extract.py # extract_bracket_elements() (entry point of the tag parser)
449
583
  export_html.py # HTML generation for tag elements
450
584
  listing.py # List processing
451
585
  code.py # Code syntax highlighting (Pygments)
452
586
  wrap_html.py # HTML document wrapping
453
587
  errors.py # Structured error types
588
+ insert_in_text.py # Store/restore helpers kept for backward compatibility
589
+ test/ # pytest suite and .l.html / -out.html reference pairs
590
+ examples/ # Example sources (see examples/README.md)
454
591
  ```
455
592
 
593
+ Run the tests with `pytest` (after `pip install -e ".[dev]"`).
594
+
456
595
 
457
596
  ## License
458
597
 
459
- MIT
598
+ MIT, see [LICENSE.md](LICENSE.md).
@@ -1,12 +1,14 @@
1
1
  # LHTML — Lightweight HTML
2
2
 
3
- [![Tests](https://github.com/drohmer/lhtml/actions/workflows/tests.yml/badge.svg?branch=feature/lhtml-v2)](https://github.com/drohmer/lhtml/actions/workflows/tests.yml)
3
+ [![Tests](https://github.com/drohmer/lhtml/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/drohmer/lhtml/actions/workflows/tests.yml)
4
4
  [![PyPI](https://img.shields.io/pypi/v/lhtml-markup)](https://pypi.org/project/lhtml-markup/)
5
5
 
6
6
  LHTML is a markup language that simplifies HTML authoring with embedded CSS styling. It is designed to be **HTML-first**: raw HTML passes through untouched, and only a few shorthand symbols (`::`, `*`, `=`, `**`, `__`) trigger conversions.
7
7
 
8
8
  LHTML is used to build static websites and presentation slides, typically combined with Jinja2 templates.
9
9
 
10
+ See the [examples](examples/) and the [changelog](CHANGELOG.md).
11
+
10
12
  ## Installation
11
13
 
12
14
  From PyPI:
@@ -39,14 +41,44 @@ Dependencies (`lark`, `pygments`, `pyyaml`) are installed automatically.
39
41
  ```bash
40
42
  lhtml input.l.html # Convert to stdout
41
43
  lhtml input.l.html -o output.html # Convert to file
42
- lhtml input.l.html -w # Wrap in full HTML document
43
- lhtml input.l.html -b # Render source line breaks as <br>
44
+ lhtml input.l.html -w # Wrap in full HTML document (--wrapAuto)
45
+ lhtml input.l.html -b # Render source line breaks as <br> (--line-breaks)
44
46
  lhtml a.l.html b.l.html # Several files: a.html, b.html next to sources
45
47
  lhtml a.l.html b.l.html -o build/ # Several files into a directory
46
48
  python -m lhtml input.l.html # Alternative invocation
49
+ lhtml --version # Show the version (also lhtml.__version__)
47
50
  ```
48
51
 
49
- Files are read and written as UTF-8 (a BOM is accepted). Errors and warnings are reported on stderr with the file name, the remaining files are still processed, and the exit code is non-zero if any file failed. An input file is never overwritten (e.g. `lhtml page.html` without `-o`). Includes are looked up in the input file's directory first, then in the current directory.
52
+ Without `-o`, `page.l.html` is written to `page.html` next to its source.
53
+
54
+ - Files are read and written as UTF-8 (a BOM is accepted).
55
+ - Errors and warnings are reported on stderr with the file name. The remaining files are still processed, and the exit code is non-zero if any file failed.
56
+ - An input file is never overwritten: `lhtml page.html` (output `page.html`) is an error, as is an output that is another input of the batch, or a symbolic or hard link to one.
57
+ - Includes are looked up in the input file's directory first, then in the current directory.
58
+
59
+ With `-w`, the result is wrapped in a minimal HTML document using the `title`, `css` and `js` of the front matter:
60
+
61
+ ```html
62
+ <!DOCTYPE html>
63
+
64
+ <html lang="en">
65
+
66
+ <head>
67
+ <meta charset="utf-8">
68
+ <meta name="viewport" content="width=device-width, initial-scale=1">
69
+ <title>My Page</title>
70
+ <link rel="stylesheet" type="text/css" href="style.css">
71
+ <script src="app.js" defer></script>
72
+ </head>
73
+
74
+ <body>
75
+ <h1>Hello</h1>
76
+
77
+
78
+ </body>
79
+
80
+ </html>
81
+ ```
50
82
 
51
83
  ### Python API
52
84
 
@@ -95,13 +127,33 @@ With classes/IDs:
95
127
  ```
96
128
  * First item
97
129
  * Second item
98
- ** Nested item A
99
- ** Nested item B
100
- *** Deep nested
130
+ ** Nested item
101
131
  * Back to top level
102
132
  ```
103
133
 
104
- Produces nested `<ul><li>` structures.
134
+ Output (a nested list is placed in its own `<li>`):
135
+ ```html
136
+ <ul>
137
+ <li>
138
+ First item
139
+ </li>
140
+ <li>
141
+ Second item
142
+ </li>
143
+ <li>
144
+ <ul>
145
+ <li>
146
+ Nested item
147
+ </li>
148
+ </ul>
149
+ </li>
150
+ <li>
151
+ Back to top level
152
+ </li>
153
+ </ul>
154
+ ```
155
+
156
+ Each `*` adds one level (`***` is level 3). A list ends at the first line that is not an item.
105
157
 
106
158
 
107
159
  ### Inline Formatting
@@ -136,7 +188,7 @@ Tag names start with a letter and may contain letters, digits, `_` and `-` (usef
136
188
 
137
189
  A closing `::` may be directly followed by punctuation or HTML (`**span::[c] x ::**`, `important ::,`). In a run of colons, `::` markers are the pairs ending the run: `::::[...]` is a closing `::` followed by `::[...]`, and `x:::nl` is `x:` followed by `::nl`. A `::` between two HTML tags (as in pre-highlighted code `<span>::</span>`) is left untouched.
138
190
 
139
- A tag that is opened but never closed, or a `::` without matching opening tag, produces an `LHTMLWarning` quoting the beginning of the tag.
191
+ A tag that is opened but never closed, or a `::` without matching opening tag, produces an `LHTMLWarning` quoting the beginning of the tag. An unmatched `::` is kept as text in the output.
140
192
 
141
193
  #### Div / Span with Styles
142
194
 
@@ -200,10 +252,29 @@ Output:
200
252
  <div style="color:blue;"> short text </div>
201
253
  ```
202
254
 
255
+ #### Explicit Closing Tags
256
+
257
+ A bare `::` closes the last opened tag. To make long or nested blocks easier to read, name the tag you close with `::name[-]` (`::[-]` closes the last one, like `::`):
258
+
259
+ ```
260
+ div::[color:red;]
261
+ Red **text**
262
+ ::div[-]
263
+ ```
264
+
265
+ Output:
266
+ ```html
267
+ <div style="color:red;">
268
+ Red <strong>text</strong>
269
+ </div>
270
+ ```
271
+
272
+ If the name does not match the last opened tag, a `LHTMLWarning` is emitted and the last opened tag is closed.
273
+
203
274
 
204
275
  ### Links
205
276
 
206
- The URL of `link::`, `img::`, `video::` and `videoplay::` is never modified (`__`, `**`, `$` are kept). Parentheses that are part of the URL are kept (`Mercury_(planet)`, `fig(1).png`), while a group starting with `.` or `#` is a class/id group.
277
+ The URL of `link::`, `img::`, `video::` and `videoplay::` is never modified (`__`, `**`, `$` are kept). Parentheses that are part of the URL are kept (`Mercury_(planet)`, `fig(1).png`), while a group starting with `.` or `#` is a class/id group. The URL may contain Jinja expressions (`img::{{ base }}/photo.jpg`, `link::{{ url_for('page') }}[Home]`), which are kept unchanged.
207
278
 
208
279
  ```
209
280
  link::https://example.com[Click here]
@@ -248,7 +319,22 @@ def hello():
248
319
  code::[-]
249
320
  ````
250
321
 
251
- `include::file` directives inside a code block insert the file as raw code. Syntax highlighting is powered by Pygments. Any language supported by Pygments can be used. An empty language (`code::[]`) renders plain text; an unknown language renders plain text with a warning.
322
+ Output (Pygments HTML inside `<div class="code">`):
323
+ ```html
324
+ <div class="code"><pre><span></span><span class="k">def</span><span class="w"> </span><span class="nf">hello</span><span class="p">():</span>
325
+ ...
326
+ </pre></div>
327
+ ```
328
+
329
+ Syntax highlighting is powered by Pygments: any language supported by Pygments can be used, case-insensitively. An empty language (`code::[]`) renders plain text; an unknown language renders plain text with a warning. The built-in `c++` language is C++ with the types of the [CGP library](https://github.com/drohmer/cgp) highlighted; other languages can be added (see [Custom Code Lexers](#custom-code-lexers)).
330
+
331
+ The colors come from a Pygments stylesheet, which you must include in your page. Generate one for the `.code` class (any [Pygments style](https://pygments.org/styles/) can replace `default`):
332
+
333
+ ```bash
334
+ pygmentize -S default -f html -a .code > code.css
335
+ ```
336
+
337
+ `include::file` directives inside a code block insert the file as raw code.
252
338
 
253
339
 
254
340
  ### Spacer
@@ -336,6 +422,50 @@ Supported metadata keys:
336
422
  | `directory_include` | list | Directories to search for includes |
337
423
 
338
424
 
425
+ ## Using LHTML with Jinja2
426
+
427
+ LHTML leaves Jinja2 untouched, so a template can be written in LHTML: convert it to HTML first, then render it with Jinja2 (`pip install jinja2`).
428
+
429
+ `blog.l.html`:
430
+ ```
431
+ = {{ page.title }}
432
+
433
+ {% for post in posts %}
434
+ div::(.post)
435
+ == link::{{ post.url }}[{{ post.title }}]
436
+ {{ post.summary }} **Read more**
437
+ ::
438
+ {% endfor %}
439
+ ```
440
+
441
+ ```python
442
+ import jinja2
443
+ import lhtml
444
+
445
+ with open('blog.l.html', encoding='utf-8') as f:
446
+ template = jinja2.Template(lhtml.run(f.read()))
447
+
448
+ html = template.render(page={'title': 'Blog'},
449
+ posts=[{'url': 'first.html', 'title': 'First post', 'summary': 'Hello.'}])
450
+ ```
451
+
452
+ `lhtml.run()` produces the Jinja2 template:
453
+ ```html
454
+ <h1>{{ page.title }}</h1>
455
+
456
+
457
+ {% for post in posts %}
458
+ <div class="post">
459
+ <h2><a href="{{ post.url }}">{{ post.title }}</a></h2>
460
+
461
+ {{ post.summary }} <strong>Read more</strong>
462
+ </div>
463
+ {% endfor %}
464
+ ```
465
+
466
+ Jinja2 expressions can also be used in URLs (`img::{{ base }}/photo.jpg`) and in tag groups (`div::[color:{{ color }};]`).
467
+
468
+
339
469
  ## Plugin System
340
470
 
341
471
  ### Custom Tag Handlers
@@ -402,7 +532,7 @@ All keys for the `meta` dict passed to `lhtml.run()`:
402
532
  'title': 'Webpage', # Document title
403
533
  'css': [], # CSS files (string or list)
404
534
  'js': [], # JS files (string or list)
405
- 'directory_include': [], # Search paths for include::
535
+ 'directory_include': [cwd], # Search paths for include:: (default: current directory)
406
536
  'current_directory': '', # Base directory for video codec detection
407
537
  }
408
538
  ```
@@ -410,7 +540,9 @@ All keys for the `meta` dict passed to `lhtml.run()`:
410
540
 
411
541
  ## Design Principles
412
542
 
413
- - **HTML-first**: Raw HTML is never modified. Only LHTML syntax triggers conversions. In particular, the following are never transformed (not even by `include::` or `::#`): HTML tags and their attributes (URLs containing `__`, quoted values containing `>`, ...), `<script>` and `<style>` blocks (CSS `::before`, ...), HTML comments, and math (`$...$`, `$$...$$`, `\(...\)`, `\[...\]`, so that `$x**2$` reaches MathJax/KaTeX intact). Text between HTML tags is still processed.
543
+ - **HTML-first**: Raw HTML is never modified. Only LHTML syntax triggers conversions. In particular, the following are never transformed (not even by `include::` or `::#`): HTML tags and their attributes, including tags spanning multiple lines (URLs containing `__`, quoted values containing `>`, ...), `<script>` and `<style>` blocks (CSS `::before`, ...), HTML comments, and math (`$...$`, `$$...$$`, `\(...\)`, `\[...\]`, so that `$x**2$` reaches MathJax/KaTeX intact). Text between HTML tags is still processed.
544
+ - **Attributes stay literal**: styles, classes/IDs and HTML attributes in LHTML tag groups and headings are preserved without inline formatting (`div::(.my__class__)` keeps `my__class__`). Link labels still support formatting.
545
+ - **Template-friendly**: Jinja2 expressions (`{{ ... }}`), statements (`{% ... %}`) and comments (`{# ... #}`) pass through unchanged (see [Using LHTML with Jinja2](#using-lhtml-with-jinja2)).
414
546
  - **Island grammar**: LHTML syntax "islands" float in a sea of opaque content (HTML, Jinja2 templates, LaTeX, etc.) that passes through untouched.
415
547
  - **Minimal**: A few symbols (`::`, `=`, `*`, `**`, `__`, `` ` ``) cover most needs. No complex configuration required.
416
548
  - **Composable**: LHTML works seamlessly with Jinja2 templates, making it suitable for static site generators.
@@ -421,20 +553,27 @@ All keys for the `meta` dict passed to `lhtml.run()`:
421
553
  ```
422
554
  src/lhtml/
423
555
  __init__.py # Public API: run(), analyse_tag(), read_yaml()
556
+ __main__.py # python -m lhtml
424
557
  cli.py # Command-line interface
425
558
  pipeline.py # ProcessingPipeline, TagRegistry, LexerRegistry
426
559
  process.py # Core transformation functions
427
560
  patterns.py # Centralized regex patterns and utilities
428
561
  tag_parser.py # Lark-based parser for :: bracket syntax
429
562
  tag_element.lark # Lark grammar definition
563
+ element_extract.py # extract_bracket_elements() (entry point of the tag parser)
430
564
  export_html.py # HTML generation for tag elements
431
565
  listing.py # List processing
432
566
  code.py # Code syntax highlighting (Pygments)
433
567
  wrap_html.py # HTML document wrapping
434
568
  errors.py # Structured error types
569
+ insert_in_text.py # Store/restore helpers kept for backward compatibility
570
+ test/ # pytest suite and .l.html / -out.html reference pairs
571
+ examples/ # Example sources (see examples/README.md)
435
572
  ```
436
573
 
574
+ Run the tests with `pytest` (after `pip install -e ".[dev]"`).
575
+
437
576
 
438
577
  ## License
439
578
 
440
- MIT
579
+ MIT, see [LICENSE.md](LICENSE.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lhtml-markup"
7
- version = "2.3.0"
7
+ dynamic = ["version"]
8
8
  description = "Lightweight HTML markup language — simplifies HTML authoring with shorthand syntax"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -19,16 +19,19 @@ dependencies = [
19
19
  Homepage = "https://github.com/drohmer/lhtml"
20
20
  Repository = "https://github.com/drohmer/lhtml"
21
21
  Issues = "https://github.com/drohmer/lhtml/issues"
22
+ Changelog = "https://github.com/drohmer/lhtml/blob/main/CHANGELOG.md"
22
23
 
23
24
  [project.optional-dependencies]
24
25
  dev = [
25
26
  "pytest>=7.0",
26
- "ansicolors",
27
27
  ]
28
28
 
29
29
  [project.scripts]
30
30
  lhtml = "lhtml:main"
31
31
 
32
+ [tool.setuptools.dynamic]
33
+ version = {attr = "lhtml.__version__"}
34
+
32
35
  [tool.setuptools.packages.find]
33
36
  where = ["src"]
34
37
 
@@ -9,6 +9,8 @@ Usage:
9
9
  html = lhtml.run(text, {'wrap-auto': True, 'title': 'My Page'})
10
10
  """
11
11
 
12
+ __version__ = '2.4.1'
13
+
12
14
  from .element_extract import extract_bracket_elements
13
15
  from .insert_in_text import insert_element_from_index, remove_element_to_index
14
16
  from .wrap_html import wrap_auto
@@ -70,7 +72,7 @@ def main():
70
72
 
71
73
  __all__ = [
72
74
  # Core API
73
- 'run', 'analyse_tag', 'read_yaml', 'main',
75
+ '__version__', 'run', 'analyse_tag', 'read_yaml', 'main',
74
76
  # Processing functions
75
77
  'extract_bracket_elements',
76
78
  'process_yaml', 'process_verbatim_to_index', 'process_verbatim_back_from_index',
@@ -6,6 +6,7 @@ Usage:
6
6
  lhtml [-w] a.l.html b.l.html # Multiple files → .html next to sources
7
7
  lhtml [-w] a.l.html b.l.html -o build/ # Multiple files → output directory
8
8
  lhtml -b file.l.html # Source line breaks rendered as <br>
9
+ lhtml --version
9
10
  python -m lhtml [same options]
10
11
  """
11
12
 
@@ -14,6 +15,7 @@ import sys
14
15
  import argparse
15
16
  import warnings
16
17
 
18
+ from . import __version__
17
19
  from .errors import LHTMLError
18
20
  from .pipeline import ProcessingPipeline
19
21
  from .process import read_source
@@ -84,6 +86,8 @@ def main():
84
86
  action='store_true')
85
87
  parser.add_argument('-o', '--output',
86
88
  help='Output file (single input) or directory (multiple inputs)')
89
+ parser.add_argument('-V', '--version', action='version',
90
+ version=f'lhtml {__version__}')
87
91
  args = parser.parse_args()
88
92
 
89
93
  meta = {'directory_include': [os.getcwd() + '/']}
@@ -118,7 +122,10 @@ def main():
118
122
  continue
119
123
 
120
124
  out_path = _output_path_for(f_in, args.output)
121
- if os.path.abspath(out_path) == os.path.abspath(f_in):
125
+ if any(os.path.realpath(out_path) == os.path.realpath(source)
126
+ or (os.path.exists(out_path) and os.path.exists(source)
127
+ and os.path.samefile(out_path, source))
128
+ for source in args.inputFiles):
122
129
  raise ValueError(f'output file would overwrite the input file [{out_path}]')
123
130
  out_dir = os.path.dirname(out_path)
124
131
  if out_dir:
@@ -9,6 +9,8 @@ import html
9
9
  import os
10
10
  import re
11
11
 
12
+ from .patterns import JINJA_RE
13
+
12
14
 
13
15
  # ---------------------------------------------------------------------------
14
16
  # Attribute helpers
@@ -24,8 +26,15 @@ def _build_attrs(elements):
24
26
 
25
27
 
26
28
  def _attr(value):
27
- """Escape the double quotes of a value placed inside a double-quoted HTML attribute."""
28
- return value.replace('"', '&quot;')
29
+ """Escape the double quotes of a value placed inside a double-quoted HTML
30
+ attribute (Jinja zones are kept as is, they are rendered before HTML)."""
31
+ parts, prev = [], 0
32
+ for m in JINJA_RE.finditer(value):
33
+ parts.append(value[prev:m.start()].replace('"', '&quot;'))
34
+ parts.append(m.group(0))
35
+ prev = m.end()
36
+ parts.append(value[prev:].replace('"', '&quot;'))
37
+ return ''.join(parts)
29
38
 
30
39
 
31
40
  def export_html_element_style(text):
@@ -115,7 +124,7 @@ def export_html_video(elements, default_inline='', current_directory=''):
115
124
  parts = ['<video']
116
125
  parts.append(export_html_element_inline(elements['{}']))
117
126
  if default_inline:
118
- parts.append(f' {default_inline} ')
127
+ parts.append(f' {default_inline}')
119
128
  parts.append(export_html_element_class_and_id(elements['()']))
120
129
  parts.append(export_html_element_style(elements['[]']))
121
130
  if os.path.isfile(os.path.join(current_directory, poster_candidate)):