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.
- {lhtml_markup-2.1.0/src/lhtml_markup.egg-info → lhtml_markup-2.2.0}/PKG-INFO +1 -1
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/pyproject.toml +1 -1
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/export_html.py +9 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/process.py +18 -1
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0/src/lhtml_markup.egg-info}/PKG-INFO +1 -1
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/test/test_lhtml.py +149 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/LICENSE.md +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/README.md +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/setup.cfg +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/__init__.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/__main__.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/ast_nodes.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/cli.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/code.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/element_extract.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/errors.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/insert_in_text.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/listing.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/patterns.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/pipeline.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/tag_element.lark +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/tag_parser.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml/wrap_html.py +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/SOURCES.txt +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/dependency_links.txt +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/entry_points.txt +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/requires.txt +0 -0
- {lhtml_markup-2.1.0 → lhtml_markup-2.2.0}/src/lhtml_markup.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "lhtml-markup"
|
|
7
|
-
version = "2.
|
|
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)
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|