python-fragments 0.22__tar.gz → 0.26__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 (46) hide show
  1. {python_fragments-0.22 → python_fragments-0.26}/PKG-INFO +1 -1
  2. {python_fragments-0.22 → python_fragments-0.26}/fragments/ast_nodes.py +50 -4
  3. {python_fragments-0.22 → python_fragments-0.26}/fragments/grammar.py +15 -5
  4. {python_fragments-0.22 → python_fragments-0.26}/fragments/html/elements.py +2 -1
  5. {python_fragments-0.22 → python_fragments-0.26}/pyproject.toml +1 -1
  6. {python_fragments-0.22 → python_fragments-0.26}/python_fragments.egg-info/PKG-INFO +1 -1
  7. {python_fragments-0.22 → python_fragments-0.26}/tests/test_grammar.py +22 -8
  8. {python_fragments-0.22 → python_fragments-0.26}/README.md +0 -0
  9. {python_fragments-0.22 → python_fragments-0.26}/fragments/__init__.py +0 -0
  10. {python_fragments-0.22 → python_fragments-0.26}/fragments/cli.py +0 -0
  11. {python_fragments-0.22 → python_fragments-0.26}/fragments/html/__init__.py +0 -0
  12. {python_fragments-0.22 → python_fragments-0.26}/fragments/loader.py +0 -0
  13. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/__init__.py +0 -0
  14. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/based_proxy.py +0 -0
  15. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/__init__.py +0 -0
  16. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/code_actions.py +0 -0
  17. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/completion.py +0 -0
  18. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/definition.py +0 -0
  19. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/diagnostics.py +0 -0
  20. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/document_highlight.py +0 -0
  21. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/document_symbols.py +0 -0
  22. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/folding_range.py +0 -0
  23. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/hover.py +0 -0
  24. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/inlay_hints.py +0 -0
  25. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/lifecycle.py +0 -0
  26. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/references.py +0 -0
  27. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/rename.py +0 -0
  28. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/semantic_tokens.py +0 -0
  29. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/client_message_handlers/signature_help.py +0 -0
  30. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/file_state.py +0 -0
  31. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/message_queue.py +0 -0
  32. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/pyright_notification_handlers/__init__.py +0 -0
  33. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/pyright_notification_handlers/capability.py +0 -0
  34. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/pyright_notification_handlers/configuration.py +0 -0
  35. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/pyright_notification_handlers/diagnostics.py +0 -0
  36. {python_fragments-0.22 → python_fragments-0.26}/fragments/lsp/types.py +0 -0
  37. {python_fragments-0.22 → python_fragments-0.26}/fragments/source.py +0 -0
  38. {python_fragments-0.22 → python_fragments-0.26}/fragments/transpiler.py +0 -0
  39. {python_fragments-0.22 → python_fragments-0.26}/fragments/types.py +0 -0
  40. {python_fragments-0.22 → python_fragments-0.26}/python_fragments.egg-info/SOURCES.txt +0 -0
  41. {python_fragments-0.22 → python_fragments-0.26}/python_fragments.egg-info/dependency_links.txt +0 -0
  42. {python_fragments-0.22 → python_fragments-0.26}/python_fragments.egg-info/entry_points.txt +0 -0
  43. {python_fragments-0.22 → python_fragments-0.26}/python_fragments.egg-info/requires.txt +0 -0
  44. {python_fragments-0.22 → python_fragments-0.26}/python_fragments.egg-info/top_level.txt +0 -0
  45. {python_fragments-0.22 → python_fragments-0.26}/setup.cfg +0 -0
  46. {python_fragments-0.22 → python_fragments-0.26}/tests/test_source_map.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fragments
3
- Version: 0.22
3
+ Version: 0.26
4
4
  Summary: Modern HTML template rendering in Python
5
5
  Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
6
  License: Proprietary
@@ -241,7 +241,7 @@ class ASTComponent:
241
241
  source_start: int = field(compare=False)
242
242
  source_end: int = field(compare=False)
243
243
 
244
- name: str
244
+ name: "ASTComponentName"
245
245
  arguments: dict[str, "ASTComponentArgument"]
246
246
  children: Sequence["ASTHTMLChild"]
247
247
 
@@ -253,7 +253,8 @@ class ASTComponent:
253
253
 
254
254
  def transpile(self, transpiled_start: int) -> None:
255
255
  self.transpiled_start = transpiled_start
256
- transpiled_start = transpiled_start + len(self.name) + 2
256
+ self.name.transpile(self.transpiled_start)
257
+ transpiled_start = self.name.transpiled_end + 2
257
258
  for child in self.children:
258
259
  child.transpile(transpiled_start)
259
260
  transpiled_start = child.transpiled_end + 1
@@ -262,7 +263,7 @@ class ASTComponent:
262
263
  children = ",".join(child.transpiled_content for child in self.children)
263
264
 
264
265
  if len(self.arguments) == 0:
265
- self.transpiled_content = self.__template__.format(self.name, children, "")
266
+ self.transpiled_content = self.__template__.format(self.name.transpiled_content, children, "")
266
267
  self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
267
268
  return
268
269
 
@@ -272,10 +273,13 @@ class ASTComponent:
272
273
  transpiled_start = attribute.transpiled_end + 1
273
274
 
274
275
  attributes = ",".join(attribute.transpiled_content for attribute in self.arguments.values())
275
- self.transpiled_content = self.__template__.format(self.name, children, attributes)
276
+ self.transpiled_content = self.__template__.format(self.name.transpiled_content, children, attributes)
276
277
  self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
277
278
 
278
279
  def map_offset(self, offset: int) -> int | None:
280
+ if self.name.source_start < offset < self.name.source_end:
281
+ return self.name.map_offset(offset)
282
+
279
283
  for attribute in self.arguments.values():
280
284
  if attribute.source_start <= offset <= attribute.source_end:
281
285
  return attribute.map_offset(offset)
@@ -287,6 +291,9 @@ class ASTComponent:
287
291
  return None
288
292
 
289
293
  def unmap_offset(self, offset: int) -> int | None:
294
+ if self.name.transpiled_start < offset < self.name.transpiled_end:
295
+ return self.name.unmap_offset(offset)
296
+
290
297
  for attribute in self.arguments.values():
291
298
  if attribute.transpiled_start <= offset <= attribute.transpiled_end:
292
299
  return attribute.unmap_offset(offset)
@@ -298,6 +305,37 @@ class ASTComponent:
298
305
  return None
299
306
 
300
307
 
308
+ @dataclass(slots=True)
309
+ class ASTComponentName:
310
+ source_start: int = field(compare=False)
311
+ source_end: int = field(compare=False)
312
+
313
+ name: str
314
+
315
+ transpiled_content: str = field(init=False)
316
+ transpiled_start: int = field(init=False)
317
+ transpiled_end: int = field(init=False)
318
+
319
+ def transpile(self, offset: int) -> None:
320
+ self.transpiled_start = offset
321
+ self.transpiled_content = self.name
322
+ self.transpiled_end = offset + len(self.name)
323
+
324
+ def map_offset(self, offset: int) -> int | None:
325
+ if self.source_start <= offset <= self.source_end:
326
+ specific_offset = offset - self.source_start
327
+ return self.transpiled_start + specific_offset
328
+
329
+ return None
330
+
331
+ def unmap_offset(self, offset: int) -> int | None:
332
+ if self.transpiled_start <= offset <= self.transpiled_end:
333
+ specific_offset = offset - self.transpiled_start
334
+ return self.source_start + specific_offset
335
+
336
+ return None
337
+
338
+
301
339
  @dataclass(slots=True)
302
340
  class ASTComponentArgument:
303
341
  source_start: int = field(compare=False)
@@ -327,6 +365,10 @@ class ASTComponentArgument:
327
365
  self.transpiled_end = self.transpiled_start + len(self.transpiled_content)
328
366
 
329
367
  def map_offset(self, offset: int) -> int | None:
368
+ if self.source_start <= offset <= self.source_start + len(self.name):
369
+ specific_offset = offset - self.source_start
370
+ return self.transpiled_start + specific_offset
371
+
330
372
  if self.interpolation is None:
331
373
  return None
332
374
 
@@ -336,6 +378,10 @@ class ASTComponentArgument:
336
378
  return None
337
379
 
338
380
  def unmap_offset(self, offset: int) -> int | None:
381
+ if self.transpiled_start <= offset <= self.transpiled_start + len(self.name):
382
+ specific_offset = offset - self.transpiled_start
383
+ return self.source_start + specific_offset
384
+
339
385
  if self.interpolation is None:
340
386
  return None
341
387
 
@@ -4,6 +4,7 @@ import re
4
4
  from fragments.ast_nodes import (
5
5
  ASTComponent,
6
6
  ASTComponentArgument,
7
+ ASTComponentName,
7
8
  ASTControlNode,
8
9
  ASTFragment,
9
10
  ASTHTMLAttribute,
@@ -140,7 +141,7 @@ def expect_component(source: Source) -> tuple[Source, ASTComponent | ASTControlN
140
141
  """An pseudo-element that actually resolves into a user-defined function call."""
141
142
  source_start = source.offset
142
143
  source = expect_string(source, "<")
143
- source, name = expect_regex(source, r"[A-Z][a-zA-Z0-9_]*", "component name")
144
+ source, name = expect_component_name(source)
144
145
  source, _ = source.eat_whitespace()
145
146
 
146
147
  arguments: dict[str, ASTComponentArgument] = {}
@@ -170,12 +171,12 @@ def expect_component(source: Source) -> tuple[Source, ASTComponent | ASTControlN
170
171
  source = expect_string(source, ">")
171
172
  source, children = expect_children(source)
172
173
  source = expect_string(source, "</")
173
- source, closing_name = expect_regex(source, r"[A-Z][a-zA-Z0-9_]*", "component name")
174
+ source, closing_name = expect_component_name(source)
174
175
  source, _ = source.eat_whitespace()
175
176
  source = expect_string(source, ">")
176
177
 
177
- if name != closing_name:
178
- raise ParsingError(f"Element closed ({closing_name!r}) is not the same as currently opened element ({name!r})", source.offset)
178
+ if name.name != closing_name.name:
179
+ raise ParsingError(f"Element closed ({closing_name.name}) is not the same as currently opened element ({name.name})", source.offset)
179
180
 
180
181
  return source, ASTControlNode[ASTComponent].wrap_child(
181
182
  ASTComponent(source_start=source_start, source_end=source.offset, name=name, arguments=arguments, children=children),
@@ -184,6 +185,13 @@ def expect_component(source: Source) -> tuple[Source, ASTComponent | ASTControlN
184
185
  )
185
186
 
186
187
 
188
+ def expect_component_name(source: Source) -> tuple[Source, ASTComponentName]:
189
+ """An identifier corresponding with a Python function."""
190
+ source_start = source.offset
191
+ source, name = expect_regex(source, r"[A-Z][a-zA-Z0-9_]*", "component name")
192
+ return source, ASTComponentName(source_start, source.offset, name)
193
+
194
+
187
195
  def expect_html_comment(source: Source) -> tuple[Source, ASTHTMLComment]:
188
196
  source_start = source.offset
189
197
  source = expect_string(source, "<!--")
@@ -261,7 +269,9 @@ def expect_children(source: Source) -> tuple[Source, list[ASTHTMLChild]]:
261
269
  while not source.remaining().startswith("</"):
262
270
  source, child = expect_child(source)
263
271
  children.append(child)
264
- source, _ = source.eat_whitespace()
272
+ source_after_whitespace, _ = source.eat_whitespace()
273
+ if not source_after_whitespace.remaining().startswith("{{"):
274
+ source = source_after_whitespace
265
275
  return source, children
266
276
 
267
277
 
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  from typing import Any
3
3
  from fragments.types import Children
4
+ import html
4
5
 
5
6
 
6
7
  def sequence(children: Children) -> str:
@@ -46,7 +47,7 @@ def attribute_to_string(name: str, value: Any) -> str:
46
47
  value = list(value)
47
48
 
48
49
  if isinstance(value, dict) or isinstance(value, list):
49
- value = json.dumps(value)
50
+ value = html.escape(json.dumps(value))
50
51
 
51
52
  if isinstance(value, bool):
52
53
  value = str(value).lower()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-fragments"
7
- version = "0.22"
7
+ version = "0.26"
8
8
  description = "Modern HTML template rendering in Python"
9
9
  authors = [{ name = "The Running Algorithm", email = "services@therunningalgorithm.info" }]
10
10
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-fragments
3
- Version: 0.22
3
+ Version: 0.26
4
4
  Summary: Modern HTML template rendering in Python
5
5
  Author-email: The Running Algorithm <services@therunningalgorithm.info>
6
6
  License: Proprietary
@@ -4,6 +4,7 @@ from fragments import grammar
4
4
  from fragments.ast_nodes import (
5
5
  ASTComponent,
6
6
  ASTComponentArgument,
7
+ ASTComponentName,
7
8
  ASTControlNode,
8
9
  ASTFragment,
9
10
  ASTHTMLAttribute,
@@ -209,7 +210,7 @@ def test_component_self_closing():
209
210
  source, fragment = grammar.expect_fragment(source)
210
211
  assert source.at_end()
211
212
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
212
- ASTComponent(-1, -1, "MyComp", {}, [])
213
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [])
213
214
  ]))
214
215
 
215
216
 
@@ -218,7 +219,7 @@ def test_component_with_double_quote_argument():
218
219
  source, fragment = grammar.expect_fragment(source)
219
220
  assert source.at_end()
220
221
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
221
- ASTComponent(-1, -1, "MyComp", {
222
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {
222
223
  "name": ASTComponentArgument(-1, -1, "name", '"hello"', None)
223
224
  }, [])
224
225
  ]))
@@ -229,7 +230,7 @@ def test_component_with_single_quote_argument():
229
230
  source, fragment = grammar.expect_fragment(source)
230
231
  assert source.at_end()
231
232
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
232
- ASTComponent(-1, -1, "MyComp", {
233
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {
233
234
  "name": ASTComponentArgument(-1, -1, "name", "'hello'", None)
234
235
  }, [])
235
236
  ]))
@@ -240,7 +241,7 @@ def test_component_with_interpolation_argument():
240
241
  source, fragment = grammar.expect_fragment(source)
241
242
  assert source.at_end()
242
243
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
243
- ASTComponent(-1, -1, "MyComp", {
244
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {
244
245
  "value": ASTComponentArgument(-1, -1, "value", None, ASTInterpolation(-1, -1, "expr", 1, 1))
245
246
  }, [])
246
247
  ]))
@@ -251,7 +252,7 @@ def test_component_with_children():
251
252
  source, fragment = grammar.expect_fragment(source)
252
253
  assert source.at_end()
253
254
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
254
- ASTComponent(-1, -1, "MyComp", {}, [
255
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [
255
256
  ASTHTMLElement(-1, -1, "p", {}, [ASTHTMLText(-1, -1, "text")], False)
256
257
  ])
257
258
  ]))
@@ -262,7 +263,7 @@ def test_component_whitespace_stripped_from_children():
262
263
  source, fragment = grammar.expect_fragment(source)
263
264
  assert source.at_end()
264
265
  assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
265
- ASTComponent(-1, -1, "MyComp", {}, [
266
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, [
266
267
  ASTHTMLElement(-1, -1, "p", {}, [ASTHTMLText(-1, -1, "text")], False)
267
268
  ])
268
269
  ]))
@@ -276,7 +277,7 @@ def test_component_with_if():
276
277
  ASTControlNode(-1, -1,
277
278
  ASTInterpolation(-1, -1, "condition", 1, 1),
278
279
  None,
279
- ASTComponent(-1, -1, "MyComp", {}, []),
280
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, []),
280
281
  )
281
282
  ]))
282
283
 
@@ -289,7 +290,7 @@ def test_component_with_for():
289
290
  ASTControlNode(-1, -1,
290
291
  None,
291
292
  ASTInterpolation(-1, -1, "item in items", 1, 1),
292
- ASTComponent(-1, -1, "MyComp", {}, []),
293
+ ASTComponent(-1, -1, ASTComponentName(-1, -1, "MyComp"), {}, []),
293
294
  )
294
295
  ]))
295
296
 
@@ -361,6 +362,19 @@ def test_html_text_multiline_before_child_element():
361
362
  ]))
362
363
 
363
364
 
365
+ def test_whitespace_between_adjacent_interpolations_is_preserved():
366
+ source = Source.from_string("<><p>{{ a }} {{ b }}</p></>")
367
+ source, fragment = grammar.expect_fragment(source)
368
+ assert source.at_end()
369
+ assert _transpiled(fragment) == _transpiled(ASTFragment(-1, -1, [
370
+ ASTHTMLElement(-1, -1, "p", {}, [
371
+ ASTInterpolation(-1, -1, "a", 1, 1),
372
+ ASTHTMLText(-1, -1, " "),
373
+ ASTInterpolation(-1, -1, "b", 1, 1),
374
+ ], False)
375
+ ]))
376
+
377
+
364
378
  # ---------------------------------------------------------------------------
365
379
  # Full integration (updated for new AST structure)
366
380
  # ---------------------------------------------------------------------------