vyasa 0.3.8__tar.gz → 0.3.10__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vyasa
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: A lightweight, elegant blogging platform built with FastHTML
5
5
  Home-page: https://github.com/yeshwanth/vyasa
6
6
  Author: Yeshwanth
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vyasa"
7
- version = "0.3.8"
7
+ version = "0.3.10"
8
8
  description = "A lightweight, elegant blogging platform built with FastHTML"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  [DEFAULT]
2
2
  lib_name = vyasa
3
- version = 0.3.6
3
+ version = 0.3.7
4
4
  min_python = 3.9
5
5
  license = apache2
6
6
  status = 4
@@ -1,4 +1,4 @@
1
- __version__ = "0.3.6"
1
+ __version__ = "0.3.7"
2
2
 
3
3
  from .core import app, rt, get_root_folder, get_blog_title
4
4
 
@@ -69,6 +69,8 @@ def span_token(name, pat, attr, prec=5):
69
69
  self.option = match.group(2) if match.group(2) else None
70
70
  elif name == 'IframeEmbed':
71
71
  self.options = match.group(2) if match.group(2) else None
72
+ elif name == 'DownloadEmbed':
73
+ self.label = match.group(2) if match.group(2) else None
72
74
  T.__name__ = name
73
75
  return T
74
76
 
@@ -85,6 +87,12 @@ IframeEmbed = span_token(
85
87
  'src',
86
88
  6
87
89
  )
90
+ DownloadEmbed = span_token(
91
+ 'DownloadEmbed',
92
+ r'\[download:([^\|\]]+)(?:\|(.+))?\]',
93
+ 'path',
94
+ 6
95
+ )
88
96
 
89
97
  # Superscript and Subscript tokens with higher precedence
90
98
  class Superscript(mst.span_token.SpanToken):
@@ -295,6 +303,7 @@ class ContentRenderer(FrankenRenderer):
295
303
  allowfullscreen = True
296
304
  caption = None
297
305
  popup = False
306
+ border = 'default'
298
307
 
299
308
  # Parse options: key=value;key=value
300
309
  if options_raw:
@@ -318,6 +327,8 @@ class ContentRenderer(FrankenRenderer):
318
327
  caption = value
319
328
  elif key == 'popup':
320
329
  popup = value.lower() in ('1', 'true', 'yes', 'on')
330
+ elif key == 'border':
331
+ border = value.lower()
321
332
 
322
333
  # Break out of normal content flow for viewport widths
323
334
  break_out = 'vw' in str(width).lower()
@@ -344,8 +355,15 @@ class ContentRenderer(FrankenRenderer):
344
355
  '</div>'
345
356
  )
346
357
 
358
+ if border in ('black', 'dark'):
359
+ border_classes = 'border border-black'
360
+ elif border in ('none', 'false', '0', 'off'):
361
+ border_classes = 'border border-transparent'
362
+ else:
363
+ border_classes = 'border border-slate-200 dark:border-slate-800'
364
+
347
365
  iframe = f'''
348
- <div class="relative my-6 rounded-lg overflow-hidden border border-slate-200 dark:border-slate-800" style="{container_style}">
366
+ <div class="relative my-6 rounded-lg overflow-hidden {border_classes}" style="{container_style}">
349
367
  {fullscreen_button}
350
368
  <iframe
351
369
  id="{iframe_id}"
@@ -362,6 +380,34 @@ class ContentRenderer(FrankenRenderer):
362
380
  if caption:
363
381
  return iframe + f'<p class="text-sm text-slate-500 dark:text-slate-400 text-center mt-2">{caption}</p>'
364
382
  return iframe
383
+
384
+ def render_download_embed(self, token):
385
+ from pathlib import Path
386
+ raw_path = token.path.strip()
387
+ label = getattr(token, 'label', None)
388
+
389
+ # Resolve relative paths against current_path like normal links
390
+ if self.current_path:
391
+ root = get_root_folder().resolve()
392
+ current_file_full = root / self.current_path
393
+ current_dir = current_file_full.parent
394
+ resolved = (current_dir / raw_path).resolve()
395
+ try:
396
+ rel_path = resolved.relative_to(root).as_posix()
397
+ download_path = f"/download/{rel_path}"
398
+ except ValueError:
399
+ download_path = raw_path
400
+ else:
401
+ download_path = f"/download/{raw_path}"
402
+
403
+ if not label:
404
+ label = Path(raw_path).name
405
+
406
+ link_class = (
407
+ "text-amber-600 dark:text-amber-400 underline underline-offset-2 "
408
+ "hover:text-amber-800 dark:hover:text-amber-200 font-medium transition-colors"
409
+ )
410
+ return f'<a href="{download_path}" class="{link_class}" download>{label}</a>'
365
411
 
366
412
  def render_footnote_ref(self, token):
367
413
  self.fn_counter += 1
@@ -562,6 +608,12 @@ class ContentRenderer(FrankenRenderer):
562
608
  is_external = href.startswith(('http://', 'https://', 'mailto:', 'tel:', '//'))
563
609
  is_absolute_internal = href.startswith('/') and not href.startswith('//')
564
610
  is_relative = not is_external and not is_absolute_internal
611
+ download_flag = False
612
+ if token.title and 'download=true' in token.title.lower():
613
+ download_flag = True
614
+ if '?download=true' in href.lower():
615
+ href = re.sub(r'\?download=true', '', href, flags=re.IGNORECASE)
616
+ download_flag = True
565
617
  if is_hash:
566
618
  link_class = (
567
619
  "text-amber-600 dark:text-amber-400 underline underline-offset-2 "
@@ -593,12 +645,23 @@ class ContentRenderer(FrankenRenderer):
593
645
  is_internal = is_absolute_internal and '.' not in href.split('/')[-1]
594
646
  hx = f' hx-get="{href}" hx-target="#main-content" hx-push-url="true" hx-swap="innerHTML show:window:top"' if is_internal else ''
595
647
  ext = '' if (is_internal or is_absolute_internal or is_hash) else ' target="_blank" rel="noopener noreferrer"'
648
+ download_attr = ''
649
+ if download_flag:
650
+ download_attr = ' download'
651
+ if href.startswith('/posts/'):
652
+ download_target = href[len('/posts/'):]
653
+ elif href.startswith('/'):
654
+ download_target = href.lstrip('/')
655
+ else:
656
+ download_target = href
657
+ href = f'/download/{download_target}'
658
+ hx = ''
596
659
  # Amber/gold link styling, stands out and is accessible
597
660
  link_class = (
598
661
  "text-amber-600 dark:text-amber-400 underline underline-offset-2 "
599
662
  "hover:text-amber-800 dark:hover:text-amber-200 font-medium transition-colors"
600
663
  )
601
- return f'<a href="{href}"{hx}{ext} class="{link_class}"{title}>{inner}</a>'
664
+ return f'<a href="{href}"{hx}{ext}{download_attr} class="{link_class}"{title}>{inner}</a>'
602
665
 
603
666
 
604
667
  def postprocess_tabs(html, tab_data_store, img_dir, current_path, footnotes):
@@ -621,7 +684,7 @@ def postprocess_tabs(html, tab_data_store, img_dir, current_path, footnotes):
621
684
  for i, (_, tab_content) in enumerate(tabs):
622
685
  active = 'active' if i == 0 else ''
623
686
  # Render each tab's content as fresh markdown
624
- with ContentRenderer(YoutubeEmbed, InlineCodeAttr, Strikethrough, FootnoteRef, Superscript, Subscript, img_dir=img_dir, footnotes=footnotes, current_path=current_path) as renderer:
687
+ with ContentRenderer(YoutubeEmbed, IframeEmbed, DownloadEmbed, InlineCodeAttr, Strikethrough, FootnoteRef, Superscript, Subscript, img_dir=img_dir, footnotes=footnotes, current_path=current_path) as renderer:
625
688
  doc = mst.Document(tab_content)
626
689
  rendered = renderer.render(doc)
627
690
  html_parts.append(f'<div class="tab-panel {active}" data-tab-index="{i}">{rendered}</div>')
@@ -702,7 +765,7 @@ def from_md(content, img_dir=None, current_path=None):
702
765
  'table': 'uk-table uk-table-striped uk-table-hover uk-table-divider uk-table-middle my-6'}
703
766
 
704
767
  # Register custom tokens with renderer context manager
705
- with ContentRenderer(YoutubeEmbed, IframeEmbed, InlineCodeAttr, Strikethrough, FootnoteRef, Superscript, Subscript, img_dir=img_dir, footnotes=footnotes, current_path=current_path) as renderer:
768
+ with ContentRenderer(YoutubeEmbed, IframeEmbed, DownloadEmbed, InlineCodeAttr, Strikethrough, FootnoteRef, Superscript, Subscript, img_dir=img_dir, footnotes=footnotes, current_path=current_path) as renderer:
706
769
  doc = mst.Document(content)
707
770
  html = renderer.render(doc)
708
771
 
@@ -1924,6 +1987,33 @@ def serve_post_static(path: str, ext: str):
1924
1987
  return FileResponse(file_path)
1925
1988
  return Response(status_code=404)
1926
1989
 
1990
+ # Serve JSON attachments from blog posts (not included in fasthtml static exts)
1991
+ @rt("/posts/{path:path}.json")
1992
+ def serve_post_json(path: str):
1993
+ from starlette.responses import FileResponse
1994
+ file_path = get_root_folder() / f'{path}.json'
1995
+ if file_path.exists():
1996
+ return FileResponse(
1997
+ file_path,
1998
+ headers={"Content-Disposition": f'attachment; filename="{file_path.name}"'}
1999
+ )
2000
+ return Response(status_code=404)
2001
+
2002
+ # Generic download route for any file under the blog root
2003
+ @rt("/download/{path:path}")
2004
+ def download_file(path: str):
2005
+ from starlette.responses import FileResponse
2006
+ root = get_root_folder().resolve()
2007
+ file_path = (root / path).resolve()
2008
+ if not str(file_path).startswith(str(root) + os.sep):
2009
+ return Response(status_code=403)
2010
+ if file_path.exists() and file_path.is_file():
2011
+ return FileResponse(
2012
+ file_path,
2013
+ headers={"Content-Disposition": f'attachment; filename="{file_path.name}"'}
2014
+ )
2015
+ return Response(status_code=404)
2016
+
1927
2017
  def theme_toggle():
1928
2018
  theme_script = """on load set franken to (localStorage's __FRANKEN__ or '{}') as Object
1929
2019
  if franken's mode is 'dark' then add .dark to <html/> end
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vyasa
3
- Version: 0.3.8
3
+ Version: 0.3.10
4
4
  Summary: A lightweight, elegant blogging platform built with FastHTML
5
5
  Home-page: https://github.com/yeshwanth/vyasa
6
6
  Author: Yeshwanth
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