Compare commits

..

5 Commits

Author SHA1 Message Date
5c170a195e FaF - Ws
What does this mean?
Fast as Fuck WebServer. Now we compress and cache like hell. so its fast as fuck. 1 new requirement which isnt too bad: Pillow, for images!

Clear cache and restart the server!
2025-10-13 18:20:27 +02:00
cee104f1f5 fixed image loading 2025-10-13 12:53:12 +02:00
ccabb5395c some updates to page loading performance and general performance.
added a ServiceWorker which caches stuff
2025-10-12 17:36:11 +02:00
a7847f6bff caching system with better performance and local CDN Package fetcher. changed stuff in template to use the lcdn 2025-10-10 20:56:48 +02:00
3494298330 fixed stuff and improved the rendering engine by making inline math available using $$ and for ml $$\ncontent\n$$ 2025-10-10 17:25:31 +02:00
14 changed files with 1134 additions and 448 deletions

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@ env
target target
Cargo.lock Cargo.lock
.idea .idea
cdn
cache

View File

@@ -8,6 +8,33 @@ body {
background: #fff; background: #fff;
} }
/* Layout foundation: force the page to be full-height and use flex */
html {
height: 100%;
box-sizing: border-box;
}
*, *::before, *::after { box-sizing: inherit; }
body {
min-height: 100vh; /* robust against percentage height issues */
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
}
/* make the main content take remaining space */
main.container {
flex: 1 1 auto; /* grow and shrink as needed */
display: block; /* keep existing layout inside main */
}
/* allow footer to sit at bottom (if content short) */
footer {
margin-top: auto;
}
/* Mobile-specific adjustments */ /* Mobile-specific adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
body { body {

3
css/prism.css Normal file
View File

@@ -0,0 +1,3 @@
/* PrismJS 1.30.0
https://prismjs.com/download#themes=prism-tomorrow&languages=markup+css+clike+javascript+abap+abnf+actionscript+ada+agda+al+antlr4+apacheconf+apex+apl+applescript+aql+arduino+arff+armasm+arturo+asciidoc+aspnet+asm6502+asmatmel+autohotkey+autoit+avisynth+avro-idl+awk+bash+basic+batch+bbcode+bbj+bicep+birb+bison+bnf+bqn+brainfuck+brightscript+bro+bsl+c+csharp+cpp+cfscript+chaiscript+cil+cilkc+cilkcpp+clojure+cmake+cobol+coffeescript+concurnas+csp+cooklang+coq+crystal+css-extras+csv+cue+cypher+d+dart+dataweave+dax+dhall+diff+django+dns-zone-file+docker+dot+ebnf+editorconfig+eiffel+ejs+elixir+elm+etlua+erb+erlang+excel-formula+fsharp+factor+false+firestore-security-rules+flow+fortran+ftl+gml+gap+gcode+gdscript+gedcom+gettext+gherkin+git+glsl+gn+linker-script+go+go-module+gradle+graphql+groovy+haml+handlebars+haskell+haxe+hcl+hlsl+hoon+http+hpkp+hsts+ichigojam+icon+icu-message-format+idris+ignore+inform7+ini+io+j+java+javadoc+javadoclike+javastacktrace+jexl+jolie+jq+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+julia+keepalived+keyman+kotlin+kumir+kusto+latex+latte+less+lilypond+liquid+lisp+livescript+llvm+log+lolcode+lua+magma+makefile+markdown+markup-templating+mata+matlab+maxscript+mel+mermaid+metafont+mizar+mongodb+monkey+moonscript+n1ql+n4js+nand2tetris-hdl+naniscript+nasm+neon+nevod+nginx+nim+nix+nsis+objectivec+ocaml+odin+opencl+openqasm+oz+parigp+parser+pascal+pascaligo+psl+pcaxis+peoplecode+perl+php+phpdoc+php-extras+plant-uml+plsql+powerquery+powershell+processing+prolog+promql+properties+protobuf+pug+puppet+pure+purebasic+purescript+python+qsharp+q+qml+qore+r+racket+cshtml+jsx+tsx+reason+regex+rego+renpy+rescript+rest+rip+roboconf+robotframework+ruby+rust+sas+sass+scss+scala+scheme+shell-session+smali+smalltalk+smarty+sml+solidity+solution-file+soy+sparql+splunk-spl+sqf+sql+squirrel+stan+stata+iecst+stylus+supercollider+swift+systemd+t4-templating+t4-cs+t4-vb+tap+tcl+tt2+textile+toml+tremor+turtle+twig+typescript+typoscript+unrealscript+uorazor+uri+v+vala+vbnet+velocity+verilog+vhdl+vim+visual-basic+warpscript+wasm+web-idl+wgsl+wiki+wolfram+wren+xeora+xml-doc+xojo+xquery+yaml+yang+zig */
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}

View File

@@ -16,17 +16,3 @@ hash_list = [
"aromantic armadillo", "aromantic armadillo",
"queer quokka", "queer quokka",
] ]
LOREM_IPSUM_COMMENTS = [
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.",
"Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet.",
"At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa.",
"gay furry pride >:3 OwO get rekt Charlie Kirk",
"Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus.",
"Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias.",
"Nulla facilisi. Cras vehicula aliquet libero, sit amet lacinia nunc commodo eu. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae.",
"Proin euismod, nisl sit amet ultricies lacinia, nisl nisl aliquam nisl, eget aliquam nisl nisl sit amet nisl. Sed euismod, nisl sit amet ultricies lacinia, nisl nisl aliquam nisl, eget aliquam nisl nisl sit amet nisl.",
"Vivamus euismod, nisl sit amet ultricies lacinia, nisl nisl aliquam nisl, eget aliquam nisl nisl sit amet nisl. Sed euismod, nisl sit amet ultricies lacinia, nisl nisl aliquam nisl, eget aliquam nisl nisl sit amet nisl."
]

View File

@@ -1,131 +0,0 @@
import random
import re
from hashes.hashes import LOREM_IPSUM_COMMENTS
class Obfuscator:
def obfuscate_html(html_content):
# Generate random strings
def generate_random_string(length=8):
chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
return ''.join(random.choice(chars) for _ in range(length))
# Protect original content
protected_blocks = []
def protect_scripts(match):
protected_blocks.append(match.group(0))
return f'<!-- PROTECTED_SCRIPT_{len(protected_blocks)-1} -->'
def protect_styles(match):
protected_blocks.append(match.group(0))
return f'<!-- PROTECTED_STYLE_{len(protected_blocks)-1} -->'
# First pass: protect critical content
temp_html = re.sub(r'<script[^>]*>.*?</script>', protect_scripts, html_content, flags=re.DOTALL)
temp_html = re.sub(r'<style[^>]*>.*?</style>', protect_styles, temp_html, flags=re.DOTALL)
# Protect tables
def protect_tables(match):
protected_blocks.append(match.group(0))
return f'<!-- PROTECTED_TABLE_{len(protected_blocks)-1} -->'
temp_html = re.sub(r'<table[^>]*>.*?</table>', protect_tables, temp_html, flags=re.DOTALL)
# Clean up HTML - remove extra whitespace
lines = []
for line in temp_html.split('\n'):
cleaned_line = ' '.join(line.split())
if cleaned_line:
lines.append(cleaned_line)
# Add comments between lines (but not too many)
obfuscated_lines = []
for i, line in enumerate(lines):
# Add comment before some lines
if line and not line.startswith('<!--') and random.random() > 0.7:
lorem_comment = random.choice(LOREM_IPSUM_COMMENTS)
obfuscated_lines.append(f'<!-- {lorem_comment} -->')
obfuscated_lines.append(line)
# Add comment after some lines
if line and not line.startswith('<!--') and random.random() > 0.8:
lorem_comment = random.choice(LOREM_IPSUM_COMMENTS)
obfuscated_lines.append(f'<!-- {lorem_comment} -->')
obfuscated = '\n'.join(obfuscated_lines)
# Inject random comments between SOME elements (not all)
def inject_some_comments(html):
# Only inject between certain safe elements
safe_patterns = [
(r'(</div>)', r'\1<!--' + generate_random_string(6) + '-->'),
(r'(</p>)', r'\1<!--' + generate_random_string(6) + '-->'),
(r'(</span>)', r'\1<!--' + generate_random_string(6) + '-->'),
(r'(</h1>)', r'\1<!--' + generate_random_string(6) + '-->'),
(r'(</h2>)', r'\1<!--' + generate_random_string(6) + '-->'),
(r'(</h3>)', r'\1<!--' + generate_random_string(6) + '-->'),
]
for pattern, replacement in safe_patterns:
if random.random() > 0.5: # 50% chance to apply each pattern
html = re.sub(pattern, replacement, html)
return html
obfuscated = inject_some_comments(obfuscated)
# Add header comments (fewer to avoid breaking the document)
header_comments = [
'auto-obfuscated-' + generate_random_string(10),
'generated-' + generate_random_string(8),
]
header_block = '\n'.join([f'<!-- {comment} -->' for comment in header_comments])
obfuscated = header_block + '\n' + obfuscated
# Add footer comments
footer_comments = [
'end-' + generate_random_string(10),
'completed-' + generate_random_string(8),
]
footer_block = '\n'.join([f'<!-- {comment} -->' for comment in footer_comments])
obfuscated = obfuscated + '\n' + footer_block
# Minimal invisible characters - only in text content, not in tags
invisible_chars = ['\u200B', '\u200C']
def add_minimal_invisible(match):
text = match.group(1)
# NEVER obfuscate script-like content
if any(keyword in text for keyword in ['function', 'const ', 'var ', 'let ', 'document.', 'window.', 'getElement', 'querySelector', 'addEventListener']):
return '>' + text + '<'
# Only add to plain text content
if len(text) > 10: # Only on longer text blocks
result = []
for i, char in enumerate(text):
result.append(char)
# Very rarely add invisible chars
if i % 8 == 0 and i > 0 and random.random() > 0.8:
result.append(random.choice(invisible_chars))
return '>' + ''.join(result) + '<'
return '>' + text + '<'
obfuscated = re.sub(r'>([^<]+)<', add_minimal_invisible, obfuscated)
# Restore protected content exactly as-is
for i, protected_content in enumerate(protected_blocks):
obfuscated = obfuscated.replace(f'<!-- PROTECTED_SCRIPT_{i} -->', protected_content)
obfuscated = obfuscated.replace(f'<!-- PROTECTED_STYLE_{i} -->', protected_content)
obfuscated = obfuscated.replace(f'<!-- PROTECTED_TABLE_{i} -->', protected_content)
# Final cleanup - ensure no double newlines and remove extra spaces
obfuscated = re.sub(r'\n\n+', '\n', obfuscated)
obfuscated = re.sub(r'[ \t]+', ' ', obfuscated) # Remove extra spaces and tabs
return obfuscated

View File

@@ -1,29 +1,41 @@
# latex_extension.py
import marko import marko
import marko.block import marko.block
import marko.inline import marko.inline
from marko.md_renderer import MarkdownRenderer
import re import re
from log.Logger import Logger from log.Logger import Logger
logger = Logger() logger = Logger()
class BlockFormula(marko.block.BlockElement): class BlockFormula(marko.block.BlockElement):
pattern = re.compile(r"\$\$ *\n([\s\S]+?)^\$\$ *$", re.MULTILINE) pattern = re.compile(r"^\$\$\s*\n?([\s\S]+?)\n?\$\$\s*$", re.MULTILINE)
def __init__(self, match): def __init__(self, match):
logger.log_debug("Did shit at __init__ for blockformula") self.children = [marko.inline.RawText(match.group(1).strip())]
self.children = [marko.inline.RawText(match.group(1))]
@classmethod @classmethod
def match(cls, source): def match(cls, source):
return source.expect_re(cls.pattern) return source.expect_re(cls.pattern)
@classmethod @classmethod
def parse(cls, source): def parse(cls, source):
logger.log_debug("Did some shit with Latex")
match = source.match match = source.match
source.consume() source.consume()
return match return match
class InlineFormula(marko.inline.InlineElement):
pattern = re.compile(r"\$(?!\$)([^\$]+?)\$")
parse_children = False # Math content is raw text
def __init__(self, match):
self.children = [marko.inline.RawText(match.group(1).strip())]
@classmethod
def match(cls, source):
return source.expect_re(cls.pattern)
class Paragraph(marko.block.Paragraph): class Paragraph(marko.block.Paragraph):
override = True override = True
@classmethod @classmethod
def break_paragraph(cls, source, lazy=False): def break_paragraph(cls, source, lazy=False):
if BlockFormula.match(source): if BlockFormula.match(source):
@@ -32,11 +44,16 @@ class Paragraph(marko.block.Paragraph):
class Renderer: class Renderer:
def render_block_formula(self, element): def render_block_formula(self, element):
# Render as HTML with MathJax-compatible format # MathJax compatible block math
return '\n<div class="math-block">$$\n' + self.render_children(element) + '$$</div>\n' # logger.log_debug(f"render_block_formula@LaTeXRenderer.py returned => {element}")
return f'\n<div class="math-block">$$\n{self.render_children(element)}\n$$</div>\n'
def render_inline_formula(self, element):
# MathJax compatible inline math
# logger.log_debug(f"render_inline_formula@LaTeXRenderer.py returned => {element}")
return f'\\({self.render_children(element)}\\)'
class LaTeXExtension: class LaTeXExtension:
logger.log_debug("Did shit at __init__ for latexextension") elements = [BlockFormula, InlineFormula, Paragraph]
elements = [BlockFormula, Paragraph]
parser_mixins = [] parser_mixins = []
renderer_mixins = [Renderer] renderer_mixins = [Renderer]

View File

@@ -1,48 +1,67 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en" style="height:100%;margin:0;">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Auto Index</title> <title>Auto Index</title>
<script src="../../js/shared/theme.js"></script>
<link rel="icon" type="image/x-icon" href="../../css/favicon/favicon.ico"> <!-- Preload + async load -->
<link rel="stylesheet" href="../../css/indexer.css"> <link rel="preload" as="style" href="/css/indexer.css" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/indexer.css"></noscript>
<link rel="icon" href="/css/favicon/favicon.ico" type="image/x-icon">
<!-- Defer theme toggle -->
<script src="/js/shared/theme.js" defer></script>
<script> <script>
console.log("javascript is enabled! good!"); document.addEventListener("DOMContentLoaded", () => {
document.write('<h1 id="nojs" style="display: flex; align-items: center; cursor: pointer;" onclick="toggleDarkMode();"><img src="../../css/icons/folder.webp" alt="Folder icon" />Index of PyPost</h1>'); // Create heading dynamically
toggleDarkMode() const h = document.createElement("h1");
h.id = "nojs";
h.innerHTML = '<img src="/css/icons/folder.webp" width="40" height="40" alt="📁" loading="lazy" style="margin-right:8px;">Index of PyPost';
h.onclick = toggleDarkMode;
document.body.prepend(h);
console.log("JavaScript enabled — index enhanced!");
});
</script>
<!-- Register service worker -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/js/post/sw.js').catch(console.error);
}
</script> </script>
</head> </head>
<body> <body>
<main style="flex:1;"> <main style="flex:1;">
<noscript> <noscript>
<div style="display: inline-flex; align-items: center; flex-wrap: wrap;"> <div style="display:inline-flex;align-items:center;flex-wrap:wrap;">
<img src="../../css/icons/script.webp" width="45" height="45" alt="Script icon" style="margin-right: 8px;" /> <img src="/css/icons/script.webp" width="45" height="45" alt="Script icon" style="margin-right:8px;">
<h1 id="nojs" style="margin: 0;">Please enable Javascript!</h1> <h1 style="margin:0;">Please enable JavaScript!</h1>
</div> </div>
<p> <p>
<i><strong>If you might be wondering, what does the Script do?</strong></i><br/> <i><strong>If you might be wondering, what does the Script do?</strong></i><br>
<ul id="nonenormalul"> <ul>
<li>It strips the .HTML ending from each file you see in the list below</li> <li>Strips the .HTML ending from filenames</li>
<li>It isn't necessary, but visually tweaks the page.</li> <li>Enables search, sorting, and back navigation</li>
<li>It enables the "Back" Function for Headers</li> <li>Allows Markdown source downloads</li>
<li>It enables Searching and Sorting of the Page-Display</li> </ul>
<li>It is essential for downloading Markdown-Source</li> </p>
</ul> </noscript>
<p id="available" style="display:flex;align-items:center;gap:8px;">
<img src="/css/icons/available.webp" width="40" height="40" alt="Available" loading="lazy">
<span>Available pages:</span>
</p> </p>
</noscript>
<p id="available"> <!-- CONTENT -->
<img src="../../css/icons/available.webp" width="40" height="40" alt="Available icon" />
Available pages:
</p>
<!-- CONTENT --> <!-- Core scripts: async load -->
<script src="/js/normal.js" defer></script>
<script src="/js/search.js" defer></script>
<!-- load scripts needed for indexer --> <script src="/js/ulorder.js" defer></script>
<script src="../../js/normal.js"></script>
<script src="../../js/search.js" defer></script>
<script src="../../js/ulorder.js" defer></script>
</body> </body>
</html> </html>

View File

@@ -1,70 +1,82 @@
<!doctype html> <!doctype html>
<html lang="en" style="height:100%; margin:0;"> <html lang="en" style="height:100%;margin:0;">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>{{ title }}</title> <title>{{ title }}</title>
<link rel="stylesheet" href="../css/main.css">
<link rel="icon" type="image/x-icon" href="../css/favicon/favicon.ico">
<script src="../js/post/download.js" defer></script>
<!-- Prism.js CSS theme --> <!-- Load main stylesheet asynchronously -->
<link href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism.min.css" rel="stylesheet" /> <link rel="preload" as="style" href="/css/main.css" onload="this.rel='stylesheet'">
<link rel="preload" as="style" href="/css/prism.css" onload="this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>
<!-- Prism.js core + languages --> <link rel="icon" type="image/x-icon" href="/css/favicon/favicon.ico">
<script src="https://cdn.jsdelivr.net/npm/prismjs/prism.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-python.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs/components/prism-javascript.min.js"></script>
<!-- JSZip for downloading the files as ZIP --> <!-- Local JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.11.0/jszip.min.js"></script> <script src="/js/shared/theme.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script> <script src="/js/post/download.js" defer></script>
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" id="MathJax-script"></script> <!-- Prism (code highlighting) -->
<script src="/js/post/prism.js" defer></script>
<!-- remove if causing issues --> <!-- Dynamic MathJax loader -->
<script src="../js/post/lazyimg.js"></script> <script>
<script src="../js/shared/theme.js"></script> document.addEventListener('DOMContentLoaded', () => {
<style> const hasMath = /\$\$(.|[\r\n])*?\$\$|\$(?!\$)(.*?)\$/.test(document.body.innerHTML);
a { text-decoration: none; color: #0066cc; } if (hasMath) {
</style> const mj = document.createElement('script');
mj.src = '/package/js/mathjax.js';
mj.defer = true;
document.head.appendChild(mj);
}
});
</script>
<!-- Service worker registration -->
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/js/post/sw.js').catch(console.error);
}
</script>
</head> </head>
<body style="display:flex; flex-direction:column; min-height:100%; margin:0;">
<main class="container" style="flex:1;"> <body>
<h1 onclick="window.location.href=window.location.origin" style="cursor:pointer; display:flex; align-items:center; gap:8px; font-size:1.5em; margin:0;"> <main class="container">
<img src="../css/icons/back.webp" width="32" height="32" alt="⬅" style="display:block;" /> <h1 onclick="window.location.href=window.location.origin" style="cursor:pointer;display:flex;align-items:center;gap:8px;font-size:1.5em;margin:0;">
{{ title }} <noscript>(Enable JavaScript!)</noscript> <img src="/css/icons/back.webp" width="32" height="32" alt="⬅" loading="lazy">
</h1> {{ title }} <noscript>(Enable JavaScript!)</noscript>
<img src="../css/icons/written.webp" width="32" height="32" alt="📄" loading="lazy" style="vertical-align: middle;padding-left:40px;cursor:pointer;" onclick="toggleDarkMode();" /> </h1>
<div class="meta" style="display: inline;cursor:pointer;" onclick="toggleDarkMode();"> <img src="/css/icons/written.webp" width="32" height="32" alt="📄" loading="lazy" style="vertical-align:middle;margin-left:40px;cursor:pointer;" onclick="toggleDarkMode();" />
Written @{{ now }} <div class="meta" style="display:inline;cursor:pointer;" onclick="toggleDarkMode();">
</div> Written @{{ now }}
<hr style="margin:10px 0;" /> </div>
<div class="html-content"> <hr style="margin:10px 0;">
{{ html_body | safe }} <div class="html-content">
</div> {{ html_body | safe }}
</div>
</main> </main>
<footer style="margin-top:auto; width:100%;"> <footer style="margin-top:auto;width:100%;">
<hr style="margin:10px 0;" /> <hr style="margin:10px 0;">
<img src="../css/icons/date.webp" width="16" height="16" alt="date" loading="lazy" style="vertical-align: middle;" /> <img src="/css/icons/date.webp" width="16" height="16" alt="date" loading="lazy">
{{ timestamp }}<br/> {{ timestamp }}<br>
<img src="../css/icons/magnifier.webp" width="16" height="16" alt="Hash1" loading="lazy" style="display:inline; vertical-align:middle;" /> <img src="/css/icons/magnifier.webp" width="16" height="16" alt="Hash1" loading="lazy">
Hash 1 (<b>UTF-8</b>)<i>:{{ hash1 }}</i><br /> Hash 1 (<b>UTF-8</b>)<i>:{{ hash1 }}</i><br>
<img src="../css/icons/magnifier.webp" width="16" height="16" alt="Hash2" loading="lazy" style="display:inline; vertical-align:middle;" /> <img src="/css/icons/magnifier.webp" width="16" height="16" alt="Hash2" loading="lazy">
Hash 2 (<b>Windows-1252</b>)<i>:{{ hash2 }}</i><br /> Hash 2 (<b>Windows-1252</b>)<i>:{{ hash2 }}</i><br>
<span style="display: inline-flex; align-items: center; gap: 8px;"> <span style="display:inline-flex;align-items:center;gap:8px;">
<img src="../css/icons/save.webp" width="16" height="16" alt="Save" loading="lazy" /> <img src="/css/icons/save.webp" width="16" height="16" alt="Save" loading="lazy">
<a id="download-md">Download as Markdown <noscript>Enable JavaScript for downloads</noscript></a> <a id="download-md">Download as Markdown</a>
<span style="border-left: 1px solid #888; height: 16px;"></span> <!-- Vertical separator --> <span style="border-left:1px solid #888;height:16px;"></span>
<img src="../css/icons/script.webp" width="16" height="16" alt="Script" loading="lazy" /> <img src="/css/icons/script.webp" width="16" height="16" alt="Script" loading="lazy">
<a id="download-html">Download as HTML <noscript>Enable JavaScript for downloads</noscript></a> <a id="download-html">Download as HTML</a>
</span> </span>
</footer> </footer>
</body> </body>
</html> </html>

300
js/post/prism.js Normal file

File diff suppressed because one or more lines are too long

32
js/post/sw.js Normal file
View File

@@ -0,0 +1,32 @@
const CACHE_NAME = 'offline-v1';
const ASSETS = [
'/css/main.css',
'/js/shared/theme.js',
'/js/post/download.js',
'/js/post/lazyimg.js',
'/package/js/prism.min.js',
'/package/js/prism-python.min.js',
'/package/js/mathjax.js',
'/css/icons/back.webp',
'/css/icons/written.webp',
'/css/icons/date.webp',
'/css/icons/magnifier.webp',
'/css/icons/save.webp',
'/css/icons/script.webp'
];
self.addEventListener('install', e => {
e.waitUntil(caches.open(CACHE_NAME).then(c => c.addAll(ASSETS)));
});
self.addEventListener('fetch', e => {
e.respondWith(
caches.match(e.request).then(res => res || fetch(e.request).then(resp => {
if (e.request.url.startsWith(self.location.origin)) {
const copy = resp.clone();
caches.open(CACHE_NAME).then(c => c.put(e.request, copy));
}
return resp;
}))
);
});

View File

@@ -1,11 +1,18 @@
/*
* We autoset the CSS to darkmode if no choice is in localstorage
* Else we allow White styles cause it doesnt look too bad
*/
function toggleDarkMode() { function toggleDarkMode() {
document.body.classList.toggle("dark-mode"); document.body.classList.toggle("dark-mode");
localStorage.setItem("dark-mode", document.body.classList.contains("dark-mode")); localStorage.setItem("dark-mode", document.body.classList.contains("dark-mode"));
} }
// Apply stored preference when the page loads
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
if (localStorage.getItem("dark-mode") === "true") { const stored = localStorage.getItem("dark-mode");
if (stored === null || stored === "true") {
document.body.classList.add("dark-mode"); document.body.classList.add("dark-mode");
} else {
document.body.classList.remove("dark-mode");
} }
}); });

View File

@@ -2,127 +2,194 @@
summary: "Würfelrotation mit Matrizen die Multipliziert werden erkläret" summary: "Würfelrotation mit Matrizen die Multipliziert werden erkläret"
--- ---
# Rotation eines Würfels um die x-Achse # Rotation eines Würfels mit Matrizen
Wir wollen verstehen, wie man einen Würfel im Raum um die x-Achse dreht. ## 1. Der Würfel im Raum
## 1. Punkte eines Würfels Ein Würfel im Ursprung des Koordinatensystems (Mittelpunkt bei ( (0,0,0) )) und der Kantenlänge 2 besitzt die Eckpunkte:
Ein Würfel hat 8 Eckpunkte. Wenn wir den Würfel in der Mitte des Koordinatensystems platzieren, können wir die Punkte als Vektoren schreiben:
$$ $$
\mathbf{A} = (1, 1, 1), \quad \begin{aligned}
\mathbf{B} = (1, 1, -1), \quad A &= (1, 1, 1), & B &= (1, 1, -1), & C &= (1, -1, 1), \\
\mathbf{C} = (1, -1, 1), \quad D &= (1, -1, -1), & E &= (-1, 1, 1), & F &= (-1, 1, -1), \\
\mathbf{D} = (1, -1, -1) G &= (-1, -1, 1), & H &= (-1, -1, -1)
\end{aligned}
$$ $$
Jeder Punkt kann als Spaltenvektor dargestellt werden:
$$ $$
\mathbf{E} = (-1, 1, 1), \quad \mathbf{v} =
\mathbf{F} = (-1, 1, -1), \quad
\mathbf{G} = (-1, -1, 1), \quad
\mathbf{H} = (-1, -1, -1)
$$
Jeder Punkt hat drei Koordinaten
$$
(x', y', z')
$$
## 2. Rotationsmatrix um die x-Achse
Wenn wir einen Punkt
$$
\mathbf{v} = \begin{pmatrix} x \\ y \\ z \end{pmatrix}
$$
um die x-Achse um einen Winkel $\theta$ drehen wollen, benutzen wir die Rotationsmatrix:
$$
R_x(\theta) =
\begin{pmatrix} \begin{pmatrix}
1 & 0 & 0 \\ x \\
0 & \cos\theta & -\sin\theta \\ y \\
0 & \sin\theta & \cos\theta z
\end{pmatrix} \end{pmatrix}.
$$
**Hinweis:**
- Die x-Koordinate bleibt gleich, weil wir um die x-Achse drehen.
- y und z verändern sich je nach Winkel $\theta$.
## 3. Berechnung des neuen Punktes
Der neue Punkt $\mathbf{v}'$ nach der Drehung ist:
$$ $$
\mathbf{v}' = R_x(\theta) \mathbf{v} =
\begin{pmatrix} Eine Drehung des Würfels im Raum kann mathematisch durch **Multiplikation mit einer Rotationsmatrix** beschrieben werden.
1 & 0 & 0 \\
0 & \cos\theta & -\sin\theta \\ ---
0 & \sin\theta & \cos\theta
\end{pmatrix} ## 2. Rotationsmatrizen im 3D-Raum
\begin{pmatrix} x \\ y \\ z \end{pmatrix} =
Eine Rotationsmatrix beschreibt eine Drehung um eine bestimmte Achse. Dabei bleibt die Länge der Vektoren unverändert, ebenso die Winkel zwischen ihnen.
Es gibt drei elementare Rotationen: um die **x-Achse**, die **y-Achse** und die **z-Achse**.
---
### a) Rotation um die x-Achse
$$
\mathbf{v}' = R_x(\theta)\mathbf{v} =
\begin{pmatrix} \begin{pmatrix}
x \\ x \\
y \cos\theta - z \sin\theta \\ y \cos\theta - z \sin\theta \\
y \sin\theta + z \cos\theta y \sin\theta + z \cos\theta
\end{pmatrix} \end{pmatrix}
$$
## 4. Beispiel
Drehen wir den Punkt
$$
\mathbf{A} = (1,1,1)
$$ $$
um Die x-Koordinate bleibt bei dieser Drehung unverändert. Der Punkt
$$ $$
\theta = 90^\circ = \frac{\pi}{2} \mathbf{v}' = R_x(\theta)\mathbf{v} =
$$
Dann gilt:
$$
\cos \theta = 0, \quad \sin \theta = 1
$$
$$
\mathbf{A}' =
\begin{pmatrix} \begin{pmatrix}
1 \\ x \\
1 \cdot 0 - 1 \cdot 1 \\ y \cos\theta - z \sin\theta \\
1 \cdot 1 + 1 \cdot 0 y \sin\theta + z \cos\theta
\end{pmatrix} = \end{pmatrix}
$$
---
### b) Rotation um die y-Achse
$$
R_y(\theta) =
\begin{pmatrix} \begin{pmatrix}
1 \\ -1 \\ 1 \cos\theta & 0 & \sin\theta \\
0 & 1 & 0 \\
-\sin\theta & 0 & \cos\theta
\end{pmatrix} \end{pmatrix}
$$ $$
## 5. Tabelle aller Punkte nach Rotation Dabei bleibt die y-Koordinate gleich, x und z verändern sich.
$$ $$
\begin{array}{c|c} \mathbf{v}' = R_y(\theta)\mathbf{v} =
\text{Originalpunkt} & \text{Punkt nach Rotation} \\ \begin{pmatrix}
\hline x \cos\theta + z \sin\theta \\
A (1,1,1) & (1,-1,1) \\ y \\
B (1,1,-1) & (1,-1,-1) \\ - x \sin\theta + z \cos\theta
C (1,-1,1) & (1,-1,-1) \\ \end{pmatrix}
D (1,-1,-1) & (1,1,-1) \\
E (-1,1,1) & (-1,-1,1) \\
F (-1,1,-1) & (-1,-1,-1) \\
G (-1,-1,1) & (-1,1,1) \\
H (-1,-1,-1) & (-1,1,-1) \\
\end{array}
$$ $$
## Fazit ---
- x bleibt unverändert ### c) Rotation um die z-Achse
- y und z ändern sich je nach Winkel
- Rotationsmatrizen sind ein mächtiges Werkzeug, um Objekte im 3D-Raum zu bewegen
$$
R_z(\theta) =
\begin{pmatrix}
\cos\theta & -\sin\theta & 0 \\
\sin\theta & \cos\theta & 0 \\
0 & 0 & 1
\end{pmatrix}
$$
Hier bleibt z unverändert, während x und y rotieren.
$$
\mathbf{v}' = R_z(\theta)\mathbf{v} =
\begin{pmatrix}
x \cos\theta - y \sin\theta \\
x \sin\theta + y \cos\theta \\
z
\end{pmatrix}
$$
---
## 3. Kombination mehrerer Rotationen
Oft wird ein Objekt nacheinander um mehrere Achsen gedreht.
Die Gesamtrotation ergibt sich durch **Multiplikation der einzelnen Rotationsmatrizen**.
Beispiel:
$$
R_{\text{gesamt}} = R_x(\alpha) \cdot R_y(\beta) \cdot R_z(\gamma).
$$
Ein Punkt $v$ wird dann abgebildet auf:
$$
\mathbf{v}' = R_{\text{gesamt}}\mathbf{v}.
$$
Die Reihenfolge ist dabei wesentlich, da die Matrixmultiplikation **nicht kommutativ** ist:
$$
R_x R_y \neq R_y R_x.
$$
Das bedeutet: Zuerst um die x-Achse und danach um die y-Achse zu drehen führt zu einem anderen Ergebnis, als die Reihenfolge umzukehren.
---
## 4. Matrixmultiplikation
Die Multiplikation zweier Matrizen (A) und (B) (jeweils 3×3) ergibt eine neue Matrix (C = A \cdot B) mit den Einträgen:
$$
c_{ij} = a_{i1}b_{1j} + a_{i2}b_{2j} + a_{i3}b_{3j}.
$$
Allgemein gilt:
* Jede Zeile von (A) wird mit jeder Spalte von (B) multipliziert.
* Die Summe dieser Produkte ergibt das jeweilige Element von (C).
So kann man Schritt für Schritt die Gesamtrotationsmatrix berechnen.
---
## 5. Beispiel für ($\theta = 45^\circ$)
Für den Winkel $\theta = 45^\circ = \frac{\pi}{4}$ gilt:
$$
\cos\theta = \sin\theta = \frac{\sqrt{2}}{2}.
$$
Damit lautet die Rotationsmatrix um die x-Achse:
$$
R_x(45^\circ) =
\begin{pmatrix}
1 & 0 & 0 \\
0 & \tfrac{\sqrt{2}}{2} & -\tfrac{\sqrt{2}}{2} \\
0 & \tfrac{\sqrt{2}}{2} & \tfrac{\sqrt{2}}{2}
\end{pmatrix}
$$
Wendet man diese Matrix auf den Punkt $\mathbf{A} = (1, 1, 1)$ an, ergibt sich:
$$
\begin{aligned}
x' &= 1, \\
y' &= 1 \cdot \tfrac{\sqrt{2}}{2} - 1 \cdot \tfrac{\sqrt{2}}{2} = 0, \\
z' &= 1 \cdot \tfrac{\sqrt{2}}{2} + 1 \cdot \tfrac{\sqrt{2}}{2} = \sqrt{2}.
\end{aligned}
$$
Der neue Punkt ist also:
$$
\mathbf{A}' = (1, 0, \sqrt{2}).
$$
Der Punkt wurde damit um die x-Achse um 45° gedreht.
---
## 6. Zusammenfassung
* Eine **Rotationsmatrix** beschreibt eine Drehung im Raum.
* Die Länge eines Vektors bleibt bei der Rotation unverändert.
* Es gibt drei Grundmatrizen: $R_x(\theta)$, $R_y(\theta)$ und $R_z(\theta)$.
* Durch **Multiplikation** kann man mehrere Drehungen kombinieren.
* Die Reihenfolge der Rotationen ist **nicht vertauschbar**.
* Bei $45^\circ$ erscheinen häufig die Werte $\frac{\sqrt{2}}{2}$ und $\frac{1}{2}$.

Binary file not shown.

View File

@@ -5,8 +5,16 @@ import subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
import mimetypes import mimetypes
import json import json
from jsmin import jsmin # pip install jsmin from jsmin import jsmin
from pathlib import Path from pathlib import Path
import requests
from functools import lru_cache
import hashlib
from typing import Optional, Tuple, Dict
import gzip
import time
from PIL import Image
from io import BytesIO
from log.Logger import * from log.Logger import *
from lua import plugin_manager from lua import plugin_manager
@@ -14,13 +22,253 @@ from PyPost import extract_summary
logger = Logger() logger = Logger()
plugin_manager = plugin_manager.PluginManager() plugin_manager = plugin_manager.PluginManager()
plugin_manager.load_all() # load all plugins plugin_manager.load_all()
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
HTML_DIR = os.path.join(PROJECT_ROOT, "html") HTML_DIR = os.path.join(PROJECT_ROOT, "html")
MARKDOWN_DIR = os.path.join(PROJECT_ROOT, "markdown") MARKDOWN_DIR = os.path.join(PROJECT_ROOT, "markdown")
BASE_FILE = os.path.join(HTML_DIR, "base", "index.html") BASE_FILE = os.path.join(HTML_DIR, "base", "index.html")
LUA_DIR = Path(PROJECT_ROOT) / "lua" / "plugins" LUA_DIR = Path(PROJECT_ROOT) / "lua" / "plugins"
CACHE_DIR = os.path.join(PROJECT_ROOT, "cache")
CDN_CACHE_DIR = os.path.join(CACHE_DIR, "cdn")
IMAGE_CACHE_DIR = os.path.join(CACHE_DIR, "images")
# Image optimization settings
IMAGE_EXTENSIONS = {'.webp', '.jpg', '.jpeg', '.png', '.gif', '.ico', '.svg'}
ICON_MAX_SIZE = (128, 128) # Max dimensions for icons
STANDARD_IMAGE_MAX_SIZE = (1920, 1080) # Max dimensions for regular images
WEBP_QUALITY = 65 # Quality for WebP conversion
ICON_QUALITY = 90 # Higher quality for icons to preserve detail
# CDN Resources to fetch and cache
CDN_RESOURCES = {
"/package/css/prism.min.css": "https://cdn.jsdelivr.net/npm/prismjs/themes/prism.min.css",
"/package/js/prism.min.js": "https://cdn.jsdelivr.net/npm/prismjs/prism.min.js",
"/package/js/prism-python.min.js": "https://cdn.jsdelivr.net/npm/prismjs/components/prism-python.min.js",
"/package/js/prism-javascript.min.js": "https://cdn.jsdelivr.net/npm/prismjs/components/prism-javascript.min.js",
"/package/js/mathjax.js": "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js",
}
CDN_BASES = {
"mathjax": "https://cdn.jsdelivr.net/npm/mathjax@3/es5"
}
CACHEABLE_EXTENSIONS = {'.css', '.js', '.webp', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot', '.ico'}
# Compression settings
COMPRESS_MIME_TYPES = {'text/html', 'text/css', 'application/javascript', 'application/json', 'text/markdown', 'text/x-lua'}
MIN_COMPRESS_SIZE = 1024 # Only compress files larger than 1KB
# Session for connection pooling
session = requests.Session()
session.mount('https://', requests.adapters.HTTPAdapter(
pool_connections=10,
pool_maxsize=20,
max_retries=3
))
def ensure_cache_dirs():
"""Ensure cache directories exist"""
os.makedirs(CDN_CACHE_DIR, exist_ok=True)
os.makedirs(IMAGE_CACHE_DIR, exist_ok=True)
logger.log_info(f"Cache directories ready: {CDN_CACHE_DIR}, {IMAGE_CACHE_DIR}")
def fetch_cdn_resources():
ensure_cache_dirs()
logger.log_info("Fetching CDN resources...")
def fetch_single_resource(local_path, cdn_url):
try:
url_hash = hashlib.md5(local_path.encode()).hexdigest()
cache_file = os.path.join(CDN_CACHE_DIR, url_hash)
if os.path.exists(cache_file):
logger.log_debug(f"CDN resource already cached: {local_path}")
return True
logger.log_info(f"Fetching {cdn_url}...")
response = session.get(cdn_url, timeout=30)
response.raise_for_status()
with open(cache_file, 'wb') as f:
f.write(response.content)
logger.log_info(f"Cached CDN resource: {local_path} ({len(response.content)} bytes)")
return True
except Exception as e:
logger.log_error(f"Failed to fetch CDN resource {cdn_url}: {e}")
return False
# Parallel fetch with threads
threads = []
for local_path, cdn_url in CDN_RESOURCES.items():
t = threading.Thread(target=fetch_single_resource, args=(local_path, cdn_url))
t.start()
threads.append(t)
for t in threads:
t.join()
def fetch_cdn_resource_on_demand(local_path: str) -> Optional[bytes]:
""" On demand fetching of a CDN """
if local_path.startswith("/package/js/"):
relative_path = local_path[12:]
if any(x in relative_path for x in ["a11y/", "input/", "output/", "ui/", "sre"]):
cdn_url = f"{CDN_BASES['mathjax']}/{relative_path}"
else:
return None
try:
url_hash = hashlib.md5(local_path.encode()).hexdigest()
cache_file = os.path.join(CDN_CACHE_DIR, url_hash)
if os.path.exists(cache_file):
with open(cache_file, 'rb') as f:
return f.read()
logger.log_info(f"Fetching on-demand: {cdn_url}")
response = session.get(cdn_url, timeout=10)
response.raise_for_status()
with open(cache_file, 'wb') as f:
f.write(response.content)
logger.log_info(f"Cached on-demand: {local_path}")
return response.content
except Exception as e:
logger.log_error(f"Failed to fetch on-demand resource {cdn_url}: {e}")
return None
return None
@lru_cache(maxsize=2048)
def load_file_cached(file_path: str, is_js: bool = False, optimize_img: bool = False) -> Tuple[bytes, str]:
# Handle image optimization
if optimize_img and should_optimize_image(file_path):
return optimize_image(file_path)
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = "application/octet-stream"
with open(file_path, "rb") as f:
content = f.read()
# Minify JS files
if is_js or mime_type == "application/javascript" or file_path.endswith(".js"):
try:
content = jsmin(content.decode("utf-8")).encode("utf-8")
except Exception as err:
logger.log_error(f"Error minifying JS file {file_path}: {err}")
return content, mime_type
@lru_cache(maxsize=1024)
def compress_content(content: bytes) -> bytes:
"""LRU cached gzip compression"""
return gzip.compress(content, compresslevel=6)
def is_icon(file_path: str) -> bool:
"""Determine if file is an icon based on path or name"""
lower_path = file_path.lower()
return (
'icon' in lower_path or
'favicon' in lower_path or
file_path.endswith('.ico') or
'/icons/' in lower_path
)
def get_image_cache_path(file_path: str) -> str:
"""Generate cache path for optimized image"""
file_hash = hashlib.md5(file_path.encode()).hexdigest()
file_stat = os.stat(file_path)
# Include mtime in hash to invalidate cache when file changes
cache_key = f"{file_hash}_{int(file_stat.st_mtime)}"
return os.path.join(IMAGE_CACHE_DIR, cache_key + ".webp")
def optimize_image(file_path: str) -> Tuple[bytes, str]:
try:
# Check cache first
cache_path = get_image_cache_path(file_path)
if os.path.exists(cache_path):
with open(cache_path, 'rb') as f:
return f.read(), "image/webp"
# Open and process image
with Image.open(file_path) as img:
# Preserve transparency by converting to RGBA if needed
if img.mode == 'P':
# Palette mode - convert to RGBA to preserve transparency
img = img.convert('RGBA')
elif img.mode == 'LA':
# Grayscale with alpha - convert to RGBA
img = img.convert('RGBA')
elif img.mode not in ('RGBA', 'RGB', 'L'):
# Other modes - try to preserve alpha if present
if 'transparency' in img.info:
img = img.convert('RGBA')
else:
img = img.convert('RGB')
# If already RGBA or RGB, keep as is
# Determine if it's an icon and resize accordingly
if is_icon(file_path):
max_size = ICON_MAX_SIZE
quality = ICON_QUALITY
else:
max_size = STANDARD_IMAGE_MAX_SIZE
quality = WEBP_QUALITY
# Resize if image is larger than max size
if img.size[0] > max_size[0] or img.size[1] > max_size[1]:
img.thumbnail(max_size, Image.Resampling.LANCZOS)
logger.log_debug(f"Resized image {file_path} to {img.size}")
# Save as WebP to BytesIO with lossless for transparency
output = BytesIO()
# Use lossless=True for images with alpha channel to preserve transparency perfectly
if img.mode == 'RGBA':
img.save(output, format='WEBP', quality=quality, method=6, lossless=False)
else:
img.save(output, format='WEBP', quality=quality, method=6)
optimized_content = output.getvalue()
# Cache the optimized image
with open(cache_path, 'wb') as f:
f.write(optimized_content)
original_size = os.path.getsize(file_path)
optimized_size = len(optimized_content)
savings = ((original_size - optimized_size) / original_size) * 100
logger.log_info(f"Optimized {file_path}: {original_size} to {optimized_size} bytes ({savings:.1f}% reduction)")
return optimized_content, "image/webp"
except Exception as e:
logger.log_error(f"Error compressing image {file_path}: {e}")
# Fall back to original file
with open(file_path, 'rb') as f:
content = f.read()
mime_type, _ = mimetypes.guess_type(file_path)
return content, mime_type or "application/octet-stream"
def prewarm_image_cache():
for root, _, files in os.walk(PROJECT_ROOT):
for f in files:
if should_optimize_image(f):
optimize_image(os.path.join(root, f))
def should_optimize_image(file_path: str) -> bool:
ext = os.path.splitext(file_path)[1].lower()
# If its a svg then just return. SVG is good
return ext in IMAGE_EXTENSIONS and ext != '.svg'
def should_cache_file(file_path: str) -> bool:
ext = os.path.splitext(file_path)[1].lower()
return ext in CACHEABLE_EXTENSIONS
def get_html_files(directory=HTML_DIR): def get_html_files(directory=HTML_DIR):
html_files = [] html_files = []
@@ -30,8 +278,17 @@ def get_html_files(directory=HTML_DIR):
html_files.append(entry) html_files.append(entry)
return html_files return html_files
_index_cache = {"content": None, "timestamp": 0}
INDEX_CACHE_TTL = 300 # 300/60 = 5min
def build_index_page(force_refresh: bool = False) -> str:
"""Build index page with caching"""
global _index_cache
current_time = time.time()
if not force_refresh and _index_cache["content"] and (current_time - _index_cache["timestamp"]) < INDEX_CACHE_TTL:
return _index_cache["content"]
def build_index_page() -> str:
with open(BASE_FILE, "r", encoding="utf-8") as f: with open(BASE_FILE, "r", encoding="utf-8") as f:
base_html = f.read() base_html = f.read()
@@ -65,9 +322,13 @@ def build_index_page() -> str:
continue continue
full_content = "\n".join(articles) + "</main>" + index_footer() full_content = "\n".join(articles) + "</main>" + index_footer()
return base_html.replace("<!-- CONTENT -->", full_content) content = base_html.replace("<!-- CONTENT -->", full_content)
# Update cache
_index_cache["content"] = content
_index_cache["timestamp"] = current_time
return content
import base64 import base64
import random import random
@@ -79,13 +340,9 @@ H1 = random.choice(hash_list)
H2_CANDIDATES = [h for h in hash_list if h != H1] H2_CANDIDATES = [h for h in hash_list if h != H1]
H2 = random.choice(H2_CANDIDATES) if H2_CANDIDATES else H1 H2 = random.choice(H2_CANDIDATES) if H2_CANDIDATES else H1
# cahcing was a bad, idea, servertime got stuck. it is now a variable ;)
def index_footer() -> str: def index_footer() -> str:
tor_link = "http://7uhuxits7qfmiagkmpazxvh3rtk6aijs6pbawge3fl77y4xqjixlhkqd.onion/" tor_link = "http://7uhuxits7qfmiagkmpazxvh3rtk6aijs6pbawge3fl77y4xqjixlhkqd.onion/"
return f""" return f"""
<!-- Footer styling doesnt need to work with
flex, or anything else, because pagnation.
-->
<div class="footer"> <div class="footer">
<footer> <footer>
<p> <p>
@@ -108,82 +365,152 @@ def index_footer() -> str:
""" """
class WebServerHTTPRequestHandler(BaseHTTPRequestHandler): class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
# This is a Helper Function for the POST Endpoints def log_message(self, format, *args):
pass
def _supports_gzip(self) -> bool:
accept_encoding = self.headers.get('Accept-Encoding', '')
return 'gzip' in accept_encoding.lower()
def _send_compressed_response(self, content: bytes, mime_type: str, cache_control: str = None):
should_compress = (
self._supports_gzip() and
mime_type in COMPRESS_MIME_TYPES and
len(content) >= MIN_COMPRESS_SIZE
)
if should_compress:
# Use cached compression
compressed = compress_content(content)
self.send_response(200)
self.send_header("Content-type", mime_type)
self.send_header("Content-Encoding", "gzip")
self.send_header("Content-Length", len(compressed))
if cache_control:
self.send_header("Cache-Control", cache_control)
self.end_headers()
self.wfile.write(compressed)
else:
self.send_response(200)
self.send_header("Content-type", mime_type)
self.send_header("Content-Length", len(content))
if cache_control:
self.send_header("Cache-Control", cache_control)
self.end_headers()
self.wfile.write(content)
def _parse_post_data(self): def _parse_post_data(self):
"""Parse POST request body""" content_length = int(self.headers.get('Content-Length', 0))
import json if content_length == 0:
content_length = int(self.headers.get('Content-Length', 0)) return {}
if content_length == 0:
return {}
post_data = self.rfile.read(content_length) post_data = self.rfile.read(content_length)
content_type = self.headers.get('Content-Type', '') content_type = self.headers.get('Content-Type', '')
try: try:
if 'application/json' in content_type: if 'application/json' in content_type:
return json.loads(post_data.decode('utf-8')) return json.loads(post_data.decode('utf-8'))
elif 'application/x-www-form-urlencoded' in content_type: elif 'application/x-www-form-urlencoded' in content_type:
from urllib.parse import parse_qs from urllib.parse import parse_qs
parsed = parse_qs(post_data.decode('utf-8')) parsed = parse_qs(post_data.decode('utf-8'))
return {k: v[0] if len(v) == 1 else v for k, v in parsed.items()} return {k: v[0] if len(v) == 1 else v for k, v in parsed.items()}
else: else:
return {"raw": post_data}
except Exception as e:
logger.log_error(f"Error parsing POST data: {e}")
return {"raw": post_data} return {"raw": post_data}
except Exception as e:
logger.log_error(f"Error parsing POST data: {e}")
return {"raw": post_data}
def do_POST(self): def do_POST(self):
"""Handle POST requests - primarily for plugin routes""" """Handle POST for Plugins"""
req_path = self.path.lstrip("/") req_path = self.path.lstrip("/")
# Parse POST data post_data = self._parse_post_data()
post_data = self._parse_post_data()
# Add additional request info request_data = {
request_data = { "path": self.path,
"path": self.path, "headers": dict(self.headers),
"headers": dict(self.headers), "data": post_data,
"data": post_data, "method": "POST"
"method": "POST" }
}
# Check plugin routes plugin_result = plugin_manager.handle_request("/" + req_path, request_data, method="POST")
plugin_result = plugin_manager.handle_request("/" + req_path, request_data, method="POST") if plugin_result is not None:
if plugin_result is not None: status, headers, body = plugin_result
status, headers, body = plugin_result self.send_response(status)
self.send_response(status) for key, value in headers.items():
for key, value in headers.items(): self.send_header(key, value)
self.send_header(key, value)
self.end_headers()
if isinstance(body, str):
self.wfile.write(body.encode("utf-8"))
elif isinstance(body, bytes):
self.wfile.write(body)
else:
self.wfile.write(str(body).encode("utf-8"))
return
# No plugin handled this POST request
self.send_response(404)
self.send_header("Content-type", "application/json")
self.end_headers() self.end_headers()
error_response = json.dumps({"error": "Route not found"})
self.wfile.write(error_response.encode("utf-8"))
def do_GET(self): if isinstance(body, str):
req_path = self.path.lstrip("/") # normalize leading / self.wfile.write(body.encode("utf-8"))
elif isinstance(body, bytes):
# Handle root/index self.wfile.write(body)
if req_path == "" or req_path == "index.html": else:
content = build_index_page() self.wfile.write(str(body).encode("utf-8"))
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(content.encode("utf-8"))
return return
# CHECK PLUGIN ROUTES FIRST self.send_response(404)
self.send_header("Content-type", "application/json")
self.end_headers()
error_response = json.dumps({"error": "Route not found"})
self.wfile.write(error_response.encode("utf-8"))
def do_GET(self):
req_path = self.path.lstrip("/")
# Handle root/index with caching
if req_path == "" or req_path == "index.html":
content = build_index_page()
self._send_compressed_response(
content.encode("utf-8"),
"text/html",
"public, max-age=300" # Cache for 5 minutes
)
return
# Handle CDN package requests
if req_path.startswith("package/"):
cdn_path = "/" + req_path
if cdn_path in CDN_RESOURCES:
url_hash = hashlib.md5(cdn_path.encode()).hexdigest()
cache_file = os.path.join(CDN_CACHE_DIR, url_hash)
if os.path.exists(cache_file):
with open(cache_file, 'rb') as f:
cached_content = f.read()
else:
cached_content = None
else:
cached_content = fetch_cdn_resource_on_demand(cdn_path)
if cached_content:
if cdn_path.endswith('.css'):
mime_type = "text/css"
elif cdn_path.endswith('.js'):
mime_type = "application/javascript"
elif cdn_path.endswith('.wasm'):
mime_type = "application/wasm"
elif cdn_path.endswith('.json'):
mime_type = "application/json"
else:
mime_type = "application/octet-stream"
self.send_response(200)
self.send_header("Content-type", mime_type)
self.send_header("Cache-Control", "public, max-age=86400")
self.send_header("Access-Control-Allow-Origin", "*")
self.end_headers()
self.wfile.write(cached_content)
return
else:
logger.log_warning(f"CDN resource not found: {cdn_path}")
self.send_response(404)
self.end_headers()
self.wfile.write(b"404 - CDN resource not available")
return
# check the plugins routes
plugin_result = plugin_manager.handle_request("/" + req_path, {"path": self.path}) plugin_result = plugin_manager.handle_request("/" + req_path, {"path": self.path})
if plugin_result is not None: if plugin_result is not None:
status, headers, body = plugin_result status, headers, body = plugin_result
@@ -196,9 +523,8 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
# Handle markdown file downloads # Handle markdown file downloads
if req_path.startswith("markdown/"): if req_path.startswith("markdown/"):
markdown_filename = req_path[9:] # Remove "markdown/" prefix markdown_filename = req_path[9:]
# Security check
if not markdown_filename.endswith(".md") or ".." in markdown_filename or "/" in markdown_filename: if not markdown_filename.endswith(".md") or ".." in markdown_filename or "/" in markdown_filename:
self.send_response(403) self.send_response(403)
self.end_headers() self.end_headers()
@@ -225,11 +551,7 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
with open(markdown_file_path, "rb") as f: with open(markdown_file_path, "rb") as f:
content = f.read() content = f.read()
self.send_response(200) self._send_compressed_response(content, "text/markdown")
self.send_header("Content-type", "text/markdown")
self.send_header("Content-Disposition", f'attachment; filename="{markdown_filename}"')
self.end_headers()
self.wfile.write(content)
logger.log_info(f"Served markdown file: {markdown_filename}") logger.log_info(f"Served markdown file: {markdown_filename}")
return return
@@ -242,9 +564,8 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
# Handle Lua file downloads # Handle Lua file downloads
if req_path.startswith("lua/"): if req_path.startswith("lua/"):
lua_filename = req_path[4:] # Remove "lua/" prefix lua_filename = req_path[4:]
# Security check
if not lua_filename.endswith(".lua") or ".." in lua_filename or "/" in lua_filename: if not lua_filename.endswith(".lua") or ".." in lua_filename or "/" in lua_filename:
self.send_response(403) self.send_response(403)
self.end_headers() self.end_headers()
@@ -271,11 +592,7 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
with open(lua_file_path, "rb") as f: with open(lua_file_path, "rb") as f:
content = f.read() content = f.read()
self.send_response(200) self._send_compressed_response(content, "text/x-lua")
self.send_header("Content-type", "text/x-lua")
self.send_header("Content-Disposition", f'attachment; filename="{lua_filename}"')
self.end_headers()
self.wfile.write(content)
logger.log_info(f"Served Lua file: {lua_filename}") logger.log_info(f"Served Lua file: {lua_filename}")
return return
@@ -286,7 +603,7 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
self.wfile.write(b"500 - Internal Server Error") self.wfile.write(b"500 - Internal Server Error")
return return
# Handle other files (existing functionality) # Handle other files with LRU caching for static assets
file_path = os.path.normpath(os.path.join(PROJECT_ROOT, req_path)) file_path = os.path.normpath(os.path.join(PROJECT_ROOT, req_path))
if not file_path.startswith(PROJECT_ROOT): if not file_path.startswith(PROJECT_ROOT):
self.send_response(403) self.send_response(403)
@@ -295,25 +612,41 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
return return
if os.path.isfile(file_path): if os.path.isfile(file_path):
mime_type, _ = mimetypes.guess_type(file_path) try:
if mime_type is None: if should_cache_file(file_path):
mime_type = "application/octet-stream" is_js = file_path.endswith('.js')
optimize_img = should_optimize_image(file_path)
content, mime_type = load_file_cached(file_path, is_js, optimize_img)
with open(file_path, "rb") as f: self._send_compressed_response(
content = f.read() content,
mime_type,
"public, max-age=3600"
)
else:
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type is None:
mime_type = "application/octet-stream"
# Obfuscate JS on the fly with open(file_path, "rb") as f:
if mime_type == "application/javascript" or file_path.endswith(".js"): content = f.read()
try:
content = jsmin(content.decode("utf-8")).encode("utf-8")
except Exception as err:
logger.log_error(f"Error minifying JS file {file_path}: {err}")
self.send_response(200) if mime_type == "application/javascript" or file_path.endswith(".js"):
self.send_header("Content-type", mime_type) try:
self.end_headers() content = jsmin(content.decode("utf-8")).encode("utf-8")
self.wfile.write(content) except Exception as err:
return logger.log_error(f"Error minifying JS file {file_path}: {err}")
self._send_compressed_response(content, mime_type)
return
except Exception as err:
logger.log_error(f"Error serving file {file_path}: {err}")
self.send_response(500)
self.end_headers()
self.wfile.write(b"500 - Internal Server Error")
return
self.send_response(404) self.send_response(404)
self.end_headers() self.end_headers()
@@ -327,13 +660,25 @@ def run_pypost():
if __name__ == "__main__": if __name__ == "__main__":
try: try:
logger.log_info("Initializing cache directories")
ensure_cache_dirs()
logger.log_info("Initializing CDN resource cache")
fetch_cdn_resources()
logger.log_info("CDN resources ready!")
threading.Thread(target=run_pypost, daemon=True).start() threading.Thread(target=run_pypost, daemon=True).start()
logger.log_debug("Started PyPost.py in background watcher thread.") logger.log_debug("Started PyPost.py in background watcher thread.")
server_address = ("localhost", 8000) server_address = ("localhost", 8000)
httpd: HTTPServer = HTTPServer(server_address, WebServerHTTPRequestHandler) # type: ignore[arg-type] httpd: HTTPServer = HTTPServer(server_address, WebServerHTTPRequestHandler)
logger.log_info(f"Serving on http://{server_address[0]}:{server_address[1]}") logger.log_info(f"Serving on http://{server_address[0]}:{server_address[1]}")
logger.log_info(f"Icon max size: {ICON_MAX_SIZE} \n Image max size: {STANDARD_IMAGE_MAX_SIZE}")
httpd.serve_forever() httpd.serve_forever()
prewarm_image_cache()
except (Exception, KeyboardInterrupt) as e: except (Exception, KeyboardInterrupt) as e:
logger.log_info(f"Shutting down server.\n Reason: {e}") if KeyboardInterrupt:
logger.log_info(f"Shutting down server.\n Reason: KeyboardInterrupt")
else:
logger.log_info(f"Shutting down server.\n Reason: {e}")
httpd.server_close() httpd.server_close()