lhtml-markup 2.4.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.4.0 → lhtml_markup-2.4.1}/PKG-INFO +151 -14
  2. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/README.md +150 -13
  3. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/pyproject.toml +4 -1
  4. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/__init__.py +3 -1
  5. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/cli.py +4 -0
  6. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/process.py +2 -1
  7. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/PKG-INFO +151 -14
  8. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/test/test_lhtml.py +22 -1
  9. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/LICENSE.md +0 -0
  10. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/setup.cfg +0 -0
  11. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/__main__.py +0 -0
  12. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/code.py +0 -0
  13. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/element_extract.py +0 -0
  14. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/errors.py +0 -0
  15. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/export_html.py +0 -0
  16. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/insert_in_text.py +0 -0
  17. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/listing.py +0 -0
  18. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/patterns.py +0 -0
  19. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/pipeline.py +0 -0
  20. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/tag_element.lark +0 -0
  21. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/tag_parser.py +0 -0
  22. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml/wrap_html.py +0 -0
  23. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/SOURCES.txt +0 -0
  24. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/dependency_links.txt +0 -0
  25. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/entry_points.txt +0 -0
  26. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/requires.txt +0 -0
  27. {lhtml_markup-2.4.0 → lhtml_markup-2.4.1}/src/lhtml_markup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lhtml-markup
3
- Version: 2.4.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
@@ -26,6 +26,8 @@ LHTML is a markup language that simplifies HTML authoring with embedded CSS styl
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. No input file in the batch is overwritten (including through symbolic or hard links) (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,6 +271,25 @@ 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
 
@@ -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
  ```
@@ -430,8 +560,8 @@ All keys for the `meta` dict passed to `lhtml.run()`:
430
560
  ## Design Principles
431
561
 
432
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.
433
- - Styles, classes/IDs and HTML attributes in LHTML tag groups are preserved without inline formatting. Link labels still support formatting.
434
- - Jinja2 expressions (`{{ ... }}`), statements (`{% ... %}`) and comments (`{# ... #}`) pass through unchanged.
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)).
435
565
  - **Island grammar**: LHTML syntax "islands" float in a sea of opaque content (HTML, Jinja2 templates, LaTeX, etc.) that passes through untouched.
436
566
  - **Minimal**: A few symbols (`::`, `=`, `*`, `**`, `__`, `` ` ``) cover most needs. No complex configuration required.
437
567
  - **Composable**: LHTML works seamlessly with Jinja2 templates, making it suitable for static site generators.
@@ -442,20 +572,27 @@ All keys for the `meta` dict passed to `lhtml.run()`:
442
572
  ```
443
573
  src/lhtml/
444
574
  __init__.py # Public API: run(), analyse_tag(), read_yaml()
575
+ __main__.py # python -m lhtml
445
576
  cli.py # Command-line interface
446
577
  pipeline.py # ProcessingPipeline, TagRegistry, LexerRegistry
447
578
  process.py # Core transformation functions
448
579
  patterns.py # Centralized regex patterns and utilities
449
580
  tag_parser.py # Lark-based parser for :: bracket syntax
450
581
  tag_element.lark # Lark grammar definition
582
+ element_extract.py # extract_bracket_elements() (entry point of the tag parser)
451
583
  export_html.py # HTML generation for tag elements
452
584
  listing.py # List processing
453
585
  code.py # Code syntax highlighting (Pygments)
454
586
  wrap_html.py # HTML document wrapping
455
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)
456
591
  ```
457
592
 
593
+ Run the tests with `pytest` (after `pip install -e ".[dev]"`).
594
+
458
595
 
459
596
  ## License
460
597
 
461
- MIT
598
+ MIT, see [LICENSE.md](LICENSE.md).
@@ -7,6 +7,8 @@ LHTML is a markup language that simplifies HTML authoring with embedded CSS styl
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. No input file in the batch is overwritten (including through symbolic or hard links) (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,6 +252,25 @@ 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
 
@@ -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
  ```
@@ -411,8 +541,8 @@ All keys for the `meta` dict passed to `lhtml.run()`:
411
541
  ## Design Principles
412
542
 
413
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.
414
- - Styles, classes/IDs and HTML attributes in LHTML tag groups are preserved without inline formatting. Link labels still support formatting.
415
- - Jinja2 expressions (`{{ ... }}`), statements (`{% ... %}`) and comments (`{# ... #}`) pass through unchanged.
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)).
416
546
  - **Island grammar**: LHTML syntax "islands" float in a sea of opaque content (HTML, Jinja2 templates, LaTeX, etc.) that passes through untouched.
417
547
  - **Minimal**: A few symbols (`::`, `=`, `*`, `**`, `__`, `` ` ``) cover most needs. No complex configuration required.
418
548
  - **Composable**: LHTML works seamlessly with Jinja2 templates, making it suitable for static site generators.
@@ -423,20 +553,27 @@ All keys for the `meta` dict passed to `lhtml.run()`:
423
553
  ```
424
554
  src/lhtml/
425
555
  __init__.py # Public API: run(), analyse_tag(), read_yaml()
556
+ __main__.py # python -m lhtml
426
557
  cli.py # Command-line interface
427
558
  pipeline.py # ProcessingPipeline, TagRegistry, LexerRegistry
428
559
  process.py # Core transformation functions
429
560
  patterns.py # Centralized regex patterns and utilities
430
561
  tag_parser.py # Lark-based parser for :: bracket syntax
431
562
  tag_element.lark # Lark grammar definition
563
+ element_extract.py # extract_bracket_elements() (entry point of the tag parser)
432
564
  export_html.py # HTML generation for tag elements
433
565
  listing.py # List processing
434
566
  code.py # Code syntax highlighting (Pygments)
435
567
  wrap_html.py # HTML document wrapping
436
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)
437
572
  ```
438
573
 
574
+ Run the tests with `pytest` (after `pip install -e ".[dev]"`).
575
+
439
576
 
440
577
  ## License
441
578
 
442
- 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.4.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"}
@@ -29,6 +29,9 @@ dev = [
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() + '/']}
@@ -396,9 +396,10 @@ def process_code(text):
396
396
  # ---------------------------------------------------------------------------
397
397
 
398
398
  def _warn_unmatched_closing(element):
399
+ """Warn about a closing :: without opening tag; the source is kept."""
399
400
  warnings.warn(str(LHTMLTagStackError(element.get('context', ''))),
400
401
  LHTMLWarning, stacklevel=4)
401
- return '::??ERROR', True
402
+ return '', False
402
403
 
403
404
 
404
405
  def _dispatch_tag(element, tag_to_close, current_directory, registry=None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lhtml-markup
3
- Version: 2.4.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
@@ -26,6 +26,8 @@ LHTML is a markup language that simplifies HTML authoring with embedded CSS styl
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. No input file in the batch is overwritten (including through symbolic or hard links) (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,6 +271,25 @@ 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
 
@@ -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
  ```
@@ -430,8 +560,8 @@ All keys for the `meta` dict passed to `lhtml.run()`:
430
560
  ## Design Principles
431
561
 
432
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.
433
- - Styles, classes/IDs and HTML attributes in LHTML tag groups are preserved without inline formatting. Link labels still support formatting.
434
- - Jinja2 expressions (`{{ ... }}`), statements (`{% ... %}`) and comments (`{# ... #}`) pass through unchanged.
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)).
435
565
  - **Island grammar**: LHTML syntax "islands" float in a sea of opaque content (HTML, Jinja2 templates, LaTeX, etc.) that passes through untouched.
436
566
  - **Minimal**: A few symbols (`::`, `=`, `*`, `**`, `__`, `` ` ``) cover most needs. No complex configuration required.
437
567
  - **Composable**: LHTML works seamlessly with Jinja2 templates, making it suitable for static site generators.
@@ -442,20 +572,27 @@ All keys for the `meta` dict passed to `lhtml.run()`:
442
572
  ```
443
573
  src/lhtml/
444
574
  __init__.py # Public API: run(), analyse_tag(), read_yaml()
575
+ __main__.py # python -m lhtml
445
576
  cli.py # Command-line interface
446
577
  pipeline.py # ProcessingPipeline, TagRegistry, LexerRegistry
447
578
  process.py # Core transformation functions
448
579
  patterns.py # Centralized regex patterns and utilities
449
580
  tag_parser.py # Lark-based parser for :: bracket syntax
450
581
  tag_element.lark # Lark grammar definition
582
+ element_extract.py # extract_bracket_elements() (entry point of the tag parser)
451
583
  export_html.py # HTML generation for tag elements
452
584
  listing.py # List processing
453
585
  code.py # Code syntax highlighting (Pygments)
454
586
  wrap_html.py # HTML document wrapping
455
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)
456
591
  ```
457
592
 
593
+ Run the tests with `pytest` (after `pip install -e ".[dev]"`).
594
+
458
595
 
459
596
  ## License
460
597
 
461
- MIT
598
+ MIT, see [LICENSE.md](LICENSE.md).
@@ -597,7 +597,17 @@ class TestUnclosedTags:
597
597
 
598
598
  def test_extra_closing_warns_with_context(self):
599
599
  r, w = _run_with_warnings('hello\n::\n')
600
- assert '::??ERROR' in r
600
+ assert r == 'hello\n::\n'
601
+ assert any('no matching opening tag' in x for x in w)
602
+
603
+ @pytest.mark.parametrize('source, expected', [
604
+ ('a :: b', 'a :: b'),
605
+ ('a ::div[-] b', 'a ::div[-] b'),
606
+ ('span::[c] x :: y ::', '<span style="c"> x </span> y ::'),
607
+ ])
608
+ def test_unmatched_closing_is_kept_as_text(self, source, expected):
609
+ r, w = _run_with_warnings(source)
610
+ assert r == expected
601
611
  assert any('no matching opening tag' in x for x in w)
602
612
 
603
613
 
@@ -1330,3 +1340,14 @@ class TestJinjaInUrlsAndHeadings:
1330
1340
  ])
1331
1341
  def test_video_opening_tag_spacing(source, expected):
1332
1342
  assert lhtml.run(source).startswith(expected + '\n')
1343
+
1344
+
1345
+ @pytest.mark.parametrize('flag', ['--version', '-V'])
1346
+ def test_cli_version(flag, monkeypatch, capsys):
1347
+ code, out, _ = TestCli._run_cli(None, monkeypatch, capsys, flag)
1348
+ assert code == 0
1349
+ assert out == f'lhtml {lhtml.__version__}\n'
1350
+
1351
+
1352
+ def test_version_format():
1353
+ assert re.fullmatch(r'\d+\.\d+\.\d+', lhtml.__version__)
File without changes
File without changes