Build and Push Docker Container / build-and-push (push) Successful in 2m37s
File cache: - Add my_modules/filecache with metadata, blob, validator and store layers - Cache convex.get_file via AsyncCache, TTL clamped to the file expiry - Skip caching for missing, incomplete or already expired records - Add a process-local, size-bounded LRU for immutable storage blobs - Build the cache stack from FILE_CACHE_* env vars in build_file_cache - Attach the cache to the app in my_modules/app/setup.py - Expose app-bound helpers in filecache/access.py for routes Serving: - Send strong ETag, Last-Modified and Cache-Control on served files - Answer If-None-Match and If-Modified-Since with 304 from metadata only - Avoid pulling the blob from Convex on a revalidation hit - Add a short Cloudflare-CDN-Cache-Control edge window for deleted files - Record cache_status in the wide event on a 304 - Invalidate cached metadata after file update and delete in both routes Text viewer: - Add my_modules/textview to render text/plain and text/markdown as HTML - Linkify http(s) URLs and trim trailing sentence punctuation - Resolve links pointing at this instance to the referenced file - Inline image previews, mark missing and expired targets - Cap resolution at MAX_LOOKUPS backend lookups per document - Serve the viewer only for real browser navigations, never for curl or CLI - Keep raw bytes for ?raw=1, downloads and Accept headers without text/html - Add templates/side/views/webpage/files/text_view.htm with a raw link Other: - Add tests for cache layers, serving, text rendering and the route - Bump multidict to 7.2.1 in uv.lock - Update the my_helpers submodule pointer
46 lines
1.3 KiB
Python
46 lines
1.3 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
# Only http(s) is linkified; everything else stays inert plain text.
|
|
_URL_PATTERN = re.compile(r'https?://[^\s<>"\'`]+', re.IGNORECASE)
|
|
|
|
# Prose punctuation that is almost never part of the URL itself.
|
|
_TRAILING_PUNCTUATION = '.,;:!?\'"'
|
|
_BRACKET_PAIRS = {')': '(', ']': '[', '}': '{'}
|
|
|
|
def _trim_trailing(url: str) -> str:
|
|
"""Give punctuation that belongs to the surrounding sentence back to the text."""
|
|
while url:
|
|
last = url[-1]
|
|
if last in _TRAILING_PUNCTUATION:
|
|
url = url[:-1]
|
|
continue
|
|
opening = _BRACKET_PAIRS.get(last)
|
|
if opening is not None and url.count(opening) < url.count(last):
|
|
url = url[:-1]
|
|
continue
|
|
break
|
|
return url
|
|
|
|
def tokenize(text: str) -> list[tuple[str, str]]:
|
|
"""Split text into ('text', value) and ('url', value) tokens, order preserved."""
|
|
tokens: list[tuple[str, str]] = []
|
|
cursor = 0
|
|
|
|
for match in _URL_PATTERN.finditer(text):
|
|
url = _trim_trailing(match.group(0))
|
|
if not url:
|
|
continue
|
|
|
|
start = match.start()
|
|
end = start + len(url)
|
|
if start > cursor:
|
|
tokens.append(('text', text[cursor:start]))
|
|
tokens.append(('url', url))
|
|
cursor = end
|
|
|
|
if cursor < len(text):
|
|
tokens.append(('text', text[cursor:]))
|
|
return tokens
|