lhtml-markup 2.1.0__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 (28) hide show
  1. {lhtml_markup-2.1.0/src/lhtml_markup.egg-info → lhtml_markup-2.2.0}/PKG-INFO +1 -1
  2. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/pyproject.toml +1 -1
  3. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/export_html.py +9 -0
  4. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/process.py +18 -1
  5. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0/src/lhtml_markup.egg-info}/PKG-INFO +1 -1
  6. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/test/test_lhtml.py +149 -0
  7. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/LICENSE.md +0 -0
  8. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/README.md +0 -0
  9. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/setup.cfg +0 -0
  10. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/__init__.py +0 -0
  11. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/__main__.py +0 -0
  12. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/ast_nodes.py +0 -0
  13. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/cli.py +0 -0
  14. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/code.py +0 -0
  15. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/element_extract.py +0 -0
  16. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/errors.py +0 -0
  17. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/insert_in_text.py +0 -0
  18. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/listing.py +0 -0
  19. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/patterns.py +0 -0
  20. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/pipeline.py +0 -0
  21. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/tag_element.lark +0 -0
  22. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/tag_parser.py +0 -0
  23. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/wrap_html.py +0 -0
  24. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/SOURCES.txt +0 -0
  25. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/dependency_links.txt +0 -0
  26. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/entry_points.txt +0 -0
  27. {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/requires.txt +0 -0
  28. {lhtml_markup-2.1.0 → 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.1.0
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.1.0"
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"}
@@ -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.1.0
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
  # ---------------------------------------------------------------------------
File without changes
File without changes
File without changes