lhtml-markup 2.0.1__tar.gz → 2.2.0__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 (29) hide show
  1. {lhtml_markup-2.0.1/src/lhtml_markup.egg-info → lhtml_markup-2.2.0}/PKG-INFO +1 -1
  2. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/pyproject.toml +1 -1
  3. lhtml_markup-2.2.0/src/lhtml/cli.py +98 -0
  4. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/export_html.py +9 -0
  5. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/process.py +18 -1
  6. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0/src/lhtml_markup.egg-info}/PKG-INFO +1 -1
  7. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/test/test_lhtml.py +149 -0
  8. lhtml_markup-2.0.1/src/lhtml/cli.py +0 -62
  9. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/LICENSE.md +0 -0
  10. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/README.md +0 -0
  11. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/setup.cfg +0 -0
  12. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/__init__.py +0 -0
  13. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/__main__.py +0 -0
  14. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/ast_nodes.py +0 -0
  15. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/code.py +0 -0
  16. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/element_extract.py +0 -0
  17. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/errors.py +0 -0
  18. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/insert_in_text.py +0 -0
  19. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/listing.py +0 -0
  20. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/patterns.py +0 -0
  21. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/pipeline.py +0 -0
  22. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/tag_element.lark +0 -0
  23. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/tag_parser.py +0 -0
  24. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml/wrap_html.py +0 -0
  25. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/SOURCES.txt +0 -0
  26. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/dependency_links.txt +0 -0
  27. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/entry_points.txt +0 -0
  28. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/requires.txt +0 -0
  29. {lhtml_markup-2.0.1 → lhtml_markup-2.2.0}/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.0.1
3
+ Version: 2.2.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lhtml-markup"
7
- version = "2.0.1"
7
+ version = "2.2.0"
8
8
  description = "Lightweight HTML markup language — simplifies HTML authoring with shorthand syntax"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,98 @@
1
+ """LHTML command-line interface.
2
+
3
+ Usage:
4
+ lhtml [-w] file.l.html # Single file → stdout
5
+ lhtml [-w] file.l.html -o output.html # Single file → output file
6
+ lhtml [-w] a.l.html b.l.html # Multiple files → .html next to sources
7
+ lhtml [-w] a.l.html b.l.html -o build/ # Multiple files → output directory
8
+ python -m lhtml [same options]
9
+ """
10
+
11
+ import os
12
+ import argparse
13
+
14
+ from .pipeline import ProcessingPipeline
15
+
16
+
17
+ _pipeline = ProcessingPipeline()
18
+
19
+
20
+ def _ensure_trailing_newline(text):
21
+ if text and text[-1] != '\n':
22
+ return text + '\n'
23
+ return text
24
+
25
+
26
+ def _output_path_for(input_path, output_arg):
27
+ """Determine the output path for a given input file.
28
+
29
+ If output_arg is a directory, write <dir>/<basename>.html.
30
+ If output_arg is a file path, return it directly (single-file mode).
31
+ If output_arg is None, derive .html from the input path.
32
+ """
33
+ basename = os.path.basename(input_path)
34
+ name, _ = os.path.splitext(basename)
35
+ # Strip double extensions like .l.html → .html
36
+ if name.endswith('.l'):
37
+ name = name[:-2]
38
+
39
+ if output_arg is None:
40
+ # Write next to source
41
+ return os.path.join(os.path.dirname(input_path), name + '.html')
42
+
43
+ if os.path.isdir(output_arg) or output_arg.endswith('/'):
44
+ os.makedirs(output_arg, exist_ok=True)
45
+ return os.path.join(output_arg, name + '.html')
46
+
47
+ # Treat as a direct file path (single-file mode)
48
+ return output_arg
49
+
50
+
51
+ def _process_file(f_in, meta_base):
52
+ """Process a single LHTML file and return the HTML output."""
53
+ meta = dict(meta_base)
54
+ dir_to_include = os.path.dirname(os.path.abspath(f_in))
55
+ if dir_to_include:
56
+ meta['directory_include'] = meta.get('directory_include', []) + [dir_to_include + '/']
57
+
58
+ with open(f_in) as fid:
59
+ txt = _ensure_trailing_newline(fid.read())
60
+
61
+ return _ensure_trailing_newline(_pipeline.run(txt, meta))
62
+
63
+
64
+ def main():
65
+ """CLI entry point."""
66
+ parser = argparse.ArgumentParser(description='Lightweight HTML')
67
+ parser.add_argument('inputFiles', nargs='+', help='Input file(s)')
68
+ parser.add_argument('-w', '--wrapAuto',
69
+ help='Wrap content in basic HTML template',
70
+ action='store_true')
71
+ parser.add_argument('-o', '--output',
72
+ help='Output file (single input) or directory (multiple inputs)')
73
+ args = parser.parse_args()
74
+
75
+ meta = {'directory_include': [os.getcwd() + '/']}
76
+ if args.wrapAuto:
77
+ meta['wrap-auto'] = True
78
+
79
+ single_file = len(args.inputFiles) == 1
80
+ single_to_stdout = single_file and args.output is None
81
+
82
+ for f_in in args.inputFiles:
83
+ if not os.path.isfile(f_in):
84
+ print(f'Error: file not found [{f_in}]')
85
+ continue
86
+
87
+ html = _process_file(f_in, meta)
88
+
89
+ if single_to_stdout:
90
+ print(html)
91
+ else:
92
+ out_path = _output_path_for(f_in, args.output)
93
+ with open(out_path, 'w') as f_out:
94
+ f_out.write(html)
95
+
96
+
97
+ if __name__ == '__main__':
98
+ main()
@@ -145,3 +145,12 @@ def export_html_video(elements, default_inline='', current_directory=''):
145
145
  def check_is_closing_tag(elements):
146
146
  """A bare :: (empty tag, span of exactly 2) is a closing tag."""
147
147
  return elements['tag'] == '' and elements['index_end'] - elements['index_start'] == 2
148
+
149
+
150
+ def check_is_explicit_closing_tag(elements):
151
+ """A named closing tag like ::div[-] or ::span[-].
152
+
153
+ Parsed as: tag='', text='div'/'span', []= '-'
154
+ (because the tag name appears after ::, not before)
155
+ """
156
+ return elements['text'] != '' and elements['[]'] == '-'
@@ -15,7 +15,7 @@ from .element_extract import extract_bracket_elements
15
15
  from .export_html import (
16
16
  export_html_element_class_and_id, export_html_generic,
17
17
  export_html_link, export_html_img, export_html_video,
18
- check_is_closing_tag,
18
+ check_is_closing_tag, check_is_explicit_closing_tag,
19
19
  )
20
20
  from .listing import process_listing # noqa: F401 — re-exported
21
21
  from .code import export_html_code
@@ -174,6 +174,23 @@ def _dispatch_tag(element, tag_to_close, current_directory, registry=None):
174
174
  """
175
175
  tag = element['tag']
176
176
 
177
+ # Explicit closing tag: ::div[-], ::span[-], etc.
178
+ if check_is_explicit_closing_tag(element):
179
+ closing_name = element['text']
180
+ if not tag_to_close:
181
+ warnings.warn(
182
+ str(LHTMLTagStackError(source_pos=element.get('index_start', -1))),
183
+ stacklevel=3,
184
+ )
185
+ return '::??ERROR', True
186
+ if tag_to_close[-1] != closing_name:
187
+ warnings.warn(
188
+ f'Closing ::{closing_name}[-] but last opened tag is <{tag_to_close[-1]}> '
189
+ f'(position {element.get("index_start", -1)})',
190
+ stacklevel=3,
191
+ )
192
+ return '</' + tag_to_close.pop() + '>', True
193
+
177
194
  # Try plugin registry first
178
195
  if registry is not None:
179
196
  handler = registry.get(tag)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lhtml-markup
3
- Version: 2.0.1
3
+ Version: 2.2.0
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
@@ -169,6 +169,43 @@ class TestTags:
169
169
  assert 'trick::' in r
170
170
 
171
171
 
172
+ # ---------------------------------------------------------------------------
173
+ # Explicit closing tags
174
+ # ---------------------------------------------------------------------------
175
+
176
+ class TestExplicitClosing:
177
+ def test_explicit_div_close(self):
178
+ r = lhtml.run('\ndiv::[color:red;]\ncontent\n::div[-]\n')
179
+ assert '<div style="color:red;">' in r
180
+ assert '</div>' in r
181
+ assert '::div' not in r
182
+
183
+ def test_explicit_span_close(self):
184
+ r = lhtml.run('\nspan::[color:blue;] text ::span[-]\n')
185
+ # span with inline text uses :: self-closing, but ::span[-] is explicit
186
+ # Actually: span::[color:blue;] text :: is self-closing
187
+ # Let's test a block span
188
+ r = lhtml.run('\nspan::[color:blue;]\ntext\n::span[-]\n')
189
+ assert '<span style="color:blue;">' in r
190
+ assert '</span>' in r
191
+
192
+ def test_nested_explicit_close(self):
193
+ r = lhtml.run('\ndiv::[padding:10px;]\nspan::[color:red;]\ntext\n::span[-]\n::div[-]\n')
194
+ assert r.count('</span>') == 1
195
+ assert r.count('</div>') == 1
196
+
197
+ def test_bare_close_still_works(self):
198
+ r = lhtml.run('\ndiv::[color:green;]\ntext\n::\n')
199
+ assert '</div>' in r
200
+
201
+ def test_mismatch_warning(self):
202
+ with warnings.catch_warnings(record=True) as w:
203
+ warnings.simplefilter('always')
204
+ lhtml.run('\ndiv::[color:red;]\nspan::[color:blue;]\ntext\n::div[-]\n::span[-]\n')
205
+ mismatches = [x for x in w if 'but last opened' in str(x.message)]
206
+ assert len(mismatches) >= 1
207
+
208
+
172
209
  # ---------------------------------------------------------------------------
173
210
  # Verbatim tests
174
211
  # ---------------------------------------------------------------------------
@@ -207,6 +244,118 @@ class TestPluginSystem:
207
244
  assert set(reg.registered_tags()) == {'foo', 'bar'}
208
245
 
209
246
 
247
+ # ---------------------------------------------------------------------------
248
+ # Code blocks
249
+ # ---------------------------------------------------------------------------
250
+
251
+ class TestCodeBlocks:
252
+ def test_python_code_block(self):
253
+ r = lhtml.run('\ncode::[python]\ndef hello():\n pass\ncode::[-]\n')
254
+ assert '<pre>' in r
255
+ assert 'hello' in r
256
+
257
+ def test_cpp_code_block(self):
258
+ r = lhtml.run('\ncode::[c++]\nint x = 0;\ncode::[-]\n')
259
+ assert '<pre>' in r
260
+
261
+ def test_text_code_block(self):
262
+ r = lhtml.run('\ncode::[text]\nplain text\ncode::[-]\n')
263
+ assert 'plain text' in r
264
+
265
+
266
+ # ---------------------------------------------------------------------------
267
+ # Unnamed div (::() syntax)
268
+ # ---------------------------------------------------------------------------
269
+
270
+ class TestUnnamedDiv:
271
+ def test_unnamed_div_with_class(self):
272
+ r = lhtml.run('\n::(.highlight)[padding:10px;]\ncontent\n::\n')
273
+ assert 'class="highlight"' in r
274
+ assert 'style="padding:10px;"' in r
275
+ assert 'content' in r
276
+
277
+ def test_unnamed_div_style_only(self):
278
+ r = lhtml.run('\n::[color:red;]\ntext\n::\n')
279
+ assert '<div style="color:red;">' in r
280
+
281
+ def test_unnamed_div_wrapping_list(self):
282
+ r = lhtml.run('\n::(.box)\n* item one\n* item two\n::\n')
283
+ assert 'class="box"' in r
284
+ assert '<ul>' in r
285
+ assert '<li>' in r
286
+
287
+
288
+ # ---------------------------------------------------------------------------
289
+ # Inline close (:: on same line)
290
+ # ---------------------------------------------------------------------------
291
+
292
+ class TestInlineClose:
293
+ def test_empty_div_inline_close(self):
294
+ r = lhtml.run('\ndiv::[height:25px;]::\n')
295
+ assert '<div style="height:25px;"></div>' in r
296
+
297
+ def test_span_inline_close_with_class(self):
298
+ r = lhtml.run('\nspan::(.tag)[color:white;] label ::\n')
299
+ assert '<span class="tag" style="color:white;"> label </span>' in r
300
+
301
+ def test_unnamed_inline_close(self):
302
+ r = lhtml.run('\n::[margin:5px;]::\n')
303
+ assert '<div style="margin:5px;"></div>' in r
304
+
305
+
306
+ # ---------------------------------------------------------------------------
307
+ # Complex nesting
308
+ # ---------------------------------------------------------------------------
309
+
310
+ class TestComplexNesting:
311
+ def test_nested_divs_three_levels(self):
312
+ r = lhtml.run('\n::[padding:20px;]\n::[background:#eee;]\ndiv::[color:red;] deep ::\n::\n::\n')
313
+ assert '<div style="padding:20px;">' in r
314
+ assert '<div style="background:#eee;">' in r
315
+ assert '<div style="color:red;"> deep </div>' in r
316
+ assert r.count('</div>') == 3
317
+
318
+ def test_formatting_inside_nested_div(self):
319
+ r = lhtml.run('\n::[border:1px solid;]\n**bold** and __italic__\n::\n')
320
+ assert '<strong>bold</strong>' in r
321
+ assert '<em>italic</em>' in r
322
+ assert '<div style="border:1px solid;">' in r
323
+
324
+ def test_link_inside_div(self):
325
+ r = lhtml.run('\n::[padding:5px;]\nlink::https://example.com[click]\n::\n')
326
+ assert '<a href="https://example.com">click</a>' in r
327
+
328
+
329
+ # ---------------------------------------------------------------------------
330
+ # Mixed content (real-world patterns)
331
+ # ---------------------------------------------------------------------------
332
+
333
+ class TestMixedContent:
334
+ def test_heading_with_bold(self):
335
+ r = lhtml.run('\n= Title with **bold** word\n')
336
+ assert '<h1>Title with <strong>bold</strong> word</h1>' in r
337
+
338
+ def test_list_with_inline_code(self):
339
+ r = lhtml.run('\n* Use `printf` to print\n')
340
+ assert '<li>' in r
341
+ assert '<code class="code-inline">printf</code>' in r
342
+
343
+ def test_list_with_link(self):
344
+ r = lhtml.run('\n* See link::https://example.com[docs]\n')
345
+ assert '<li>' in r
346
+ assert '<a href="https://example.com">docs</a>' in r
347
+
348
+ def test_code_inside_styled_div(self):
349
+ r = lhtml.run('\n::[font-size:80%;]\ncode::[c++]\nint x;\ncode::[-]\n::\n')
350
+ assert '<div style="font-size:80%;">' in r
351
+ assert '<pre>' in r
352
+
353
+ def test_raw_html_passthrough_with_lhtml(self):
354
+ r = lhtml.run('\n<p>HTML</p>\ndiv::[color:red;] lhtml ::\n')
355
+ assert '<p>HTML</p>' in r
356
+ assert '<div style="color:red;"> lhtml </div>' in r
357
+
358
+
210
359
  # ---------------------------------------------------------------------------
211
360
  # Non-regression: real-world lab_website examples
212
361
  # ---------------------------------------------------------------------------
@@ -1,62 +0,0 @@
1
- """LHTML command-line interface.
2
-
3
- Usage:
4
- lhtml [-w] inputFile [-o outputFile]
5
- python -m lhtml [-w] inputFile [-o outputFile]
6
- """
7
-
8
- import os
9
- import argparse
10
-
11
- from .pipeline import ProcessingPipeline
12
-
13
-
14
- _pipeline = ProcessingPipeline()
15
-
16
-
17
- def _ensure_trailing_newline(text):
18
- if text and text[-1] != '\n':
19
- return text + '\n'
20
- return text
21
-
22
-
23
- def main():
24
- """CLI entry point."""
25
- parser = argparse.ArgumentParser(description='Lightweight HTML')
26
- parser.add_argument('inputFile', help='Input filepath')
27
- parser.add_argument('-w', '--wrapAuto',
28
- help='Wrap content in basic HTML template',
29
- action='store_true')
30
- parser.add_argument('-o', '--outputFile', help='Output filepath')
31
- args = parser.parse_args()
32
-
33
- meta = {}
34
- if args.wrapAuto:
35
- meta['wrap-auto'] = True
36
-
37
- meta['directory_include'] = [os.getcwd() + '/']
38
- f_in = args.inputFile
39
-
40
- if os.path.isfile(f_in):
41
- dir_to_include = os.path.dirname(f_in)
42
- if dir_to_include:
43
- meta['directory_include'].append(
44
- os.path.join(os.getcwd(), dir_to_include) + '/'
45
- )
46
-
47
- with open(f_in) as fid:
48
- txt = _ensure_trailing_newline(fid.read())
49
-
50
- html = _ensure_trailing_newline(_pipeline.run(txt, meta))
51
-
52
- if args.outputFile:
53
- with open(args.outputFile, 'w') as f_out:
54
- f_out.write(html)
55
- else:
56
- print(html)
57
- else:
58
- print(f'Error: unrecognized input file [{f_in}]')
59
-
60
-
61
- if __name__ == '__main__':
62
- main()
File without changes
File without changes
File without changes