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
Cargo.lock
.idea
cdn
cache

View File

@@ -8,6 +8,33 @@ body {
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 */
@media (max-width: 768px) {
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",
"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.block
import marko.inline
from marko.md_renderer import MarkdownRenderer
import re
from log.Logger import Logger
logger = Logger()
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):
logger.log_debug("Did shit at __init__ for blockformula")
self.children = [marko.inline.RawText(match.group(1))]
self.children = [marko.inline.RawText(match.group(1).strip())]
@classmethod
def match(cls, source):
return source.expect_re(cls.pattern)
@classmethod
def parse(cls, source):
logger.log_debug("Did some shit with Latex")
match = source.match
source.consume()
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):
override = True
@classmethod
def break_paragraph(cls, source, lazy=False):
if BlockFormula.match(source):
@@ -32,11 +44,16 @@ class Paragraph(marko.block.Paragraph):
class Renderer:
def render_block_formula(self, element):
# Render as HTML with MathJax-compatible format
return '\n<div class="math-block">$$\n' + self.render_children(element) + '$$</div>\n'
# MathJax compatible block math
# 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:
logger.log_debug("Did shit at __init__ for latexextension")
elements = [BlockFormula, Paragraph]
elements = [BlockFormula, InlineFormula, Paragraph]
parser_mixins = []
renderer_mixins = [Renderer]

View File

@@ -1,48 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" style="height:100%;margin:0;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Auto Index</title>
<script src="../../js/shared/theme.js"></script>
<link rel="icon" type="image/x-icon" href="../../css/favicon/favicon.ico">
<link rel="stylesheet" href="../../css/indexer.css">
<!-- Preload + async load -->
<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>
console.log("javascript is enabled! good!");
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>');
toggleDarkMode()
document.addEventListener("DOMContentLoaded", () => {
// Create heading dynamically
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>
</head>
<body>
<main style="flex:1;">
<noscript>
<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;" />
<h1 id="nojs" style="margin: 0;">Please enable Javascript!</h1>
<img src="/css/icons/script.webp" width="45" height="45" alt="Script icon" style="margin-right:8px;">
<h1 style="margin:0;">Please enable JavaScript!</h1>
</div>
<p>
<i><strong>If you might be wondering, what does the Script do?</strong></i><br/>
<ul id="nonenormalul">
<li>It strips the .HTML ending from each file you see in the list below</li>
<li>It isn't necessary, but visually tweaks the page.</li>
<li>It enables the "Back" Function for Headers</li>
<li>It enables Searching and Sorting of the Page-Display</li>
<li>It is essential for downloading Markdown-Source</li>
<i><strong>If you might be wondering, what does the Script do?</strong></i><br>
<ul>
<li>Strips the .HTML ending from filenames</li>
<li>Enables search, sorting, and back navigation</li>
<li>Allows Markdown source downloads</li>
</ul>
</p>
</noscript>
<p id="available">
<img src="../../css/icons/available.webp" width="40" height="40" alt="Available icon" />
Available pages:
<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>
<!-- CONTENT -->
<!-- load scripts needed for indexer -->
<script src="../../js/normal.js"></script>
<script src="../../js/search.js" defer></script>
<script src="../../js/ulorder.js" defer></script>
<!-- Core scripts: async load -->
<script src="/js/normal.js" defer></script>
<script src="/js/search.js" defer></script>
<script src="/js/ulorder.js" defer></script>
</body>
</html>

View File

@@ -4,67 +4,79 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<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 -->
<link href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism.min.css" rel="stylesheet" />
<!-- Load main stylesheet asynchronously -->
<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 -->
<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>
<link rel="icon" type="image/x-icon" href="/css/favicon/favicon.ico">
<!-- JSZip for downloading the files as ZIP -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.11.0/jszip.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
<!-- Local JS -->
<script src="/js/shared/theme.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 -->
<script src="../js/post/lazyimg.js"></script>
<script src="../js/shared/theme.js"></script>
<style>
a { text-decoration: none; color: #0066cc; }
</style>
<!-- Dynamic MathJax loader -->
<script>
document.addEventListener('DOMContentLoaded', () => {
const hasMath = /\$\$(.|[\r\n])*?\$\$|\$(?!\$)(.*?)\$/.test(document.body.innerHTML);
if (hasMath) {
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>
<body style="display:flex; flex-direction:column; min-height:100%; margin:0;">
<main class="container" style="flex:1;">
<body>
<main class="container">
<h1 onclick="window.location.href=window.location.origin" style="cursor:pointer;display:flex;align-items:center;gap:8px;font-size:1.5em;margin:0;">
<img src="../css/icons/back.webp" width="32" height="32" alt="⬅" style="display:block;" />
<img src="/css/icons/back.webp" width="32" height="32" alt="⬅" loading="lazy">
{{ title }} <noscript>(Enable JavaScript!)</noscript>
</h1>
<img src="../css/icons/written.webp" width="32" height="32" alt="📄" loading="lazy" style="vertical-align: middle;padding-left:40px;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();" />
<div class="meta" style="display:inline;cursor:pointer;" onclick="toggleDarkMode();">
Written @{{ now }}
</div>
<hr style="margin:10px 0;" />
<hr style="margin:10px 0;">
<div class="html-content">
{{ html_body | safe }}
</div>
</main>
<footer style="margin-top:auto;width:100%;">
<hr style="margin:10px 0;" />
<img src="../css/icons/date.webp" width="16" height="16" alt="date" loading="lazy" style="vertical-align: middle;" />
{{ timestamp }}<br/>
<hr style="margin:10px 0;">
<img src="/css/icons/date.webp" width="16" height="16" alt="date" loading="lazy">
{{ timestamp }}<br>
<img src="../css/icons/magnifier.webp" width="16" height="16" alt="Hash1" loading="lazy" style="display:inline; vertical-align:middle;" />
Hash 1 (<b>UTF-8</b>)<i>:{{ hash1 }}</i><br />
<img src="/css/icons/magnifier.webp" width="16" height="16" alt="Hash1" loading="lazy">
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;" />
Hash 2 (<b>Windows-1252</b>)<i>:{{ hash2 }}</i><br />
<img src="/css/icons/magnifier.webp" width="16" height="16" alt="Hash2" loading="lazy">
Hash 2 (<b>Windows-1252</b>)<i>:{{ hash2 }}</i><br>
<span style="display:inline-flex;align-items:center;gap:8px;">
<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>
<img src="/css/icons/save.webp" width="16" height="16" alt="Save" loading="lazy">
<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" />
<a id="download-html">Download as HTML <noscript>Enable JavaScript for downloads</noscript></a>
<img src="/css/icons/script.webp" width="16" height="16" alt="Script" loading="lazy">
<a id="download-html">Download as HTML</a>
</span>
</footer>
</body>
</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() {
document.body.classList.toggle("dark-mode");
localStorage.setItem("dark-mode", document.body.classList.contains("dark-mode"));
}
// Apply stored preference when the page loads
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");
} else {
document.body.classList.remove("dark-mode");
}
});

View File

@@ -2,127 +2,194 @@
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 hat 8 Eckpunkte. Wenn wir den Würfel in der Mitte des Koordinatensystems platzieren, können wir die Punkte als Vektoren schreiben:
Ein Würfel im Ursprung des Koordinatensystems (Mittelpunkt bei ( (0,0,0) )) und der Kantenlänge 2 besitzt die Eckpunkte:
$$
\mathbf{A} = (1, 1, 1), \quad
\mathbf{B} = (1, 1, -1), \quad
\mathbf{C} = (1, -1, 1), \quad
\mathbf{D} = (1, -1, -1)
\begin{aligned}
A &= (1, 1, 1), & B &= (1, 1, -1), & C &= (1, -1, 1), \\
D &= (1, -1, -1), & E &= (-1, 1, 1), & F &= (-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{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) =
\mathbf{v} =
\begin{pmatrix}
1 & 0 & 0 \\
0 & \cos\theta & -\sin\theta \\
0 & \sin\theta & \cos\theta
\end{pmatrix}
x \\
y \\
z
\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$.
Eine Drehung des Würfels im Raum kann mathematisch durch **Multiplikation mit einer Rotationsmatrix** beschrieben werden.
## 3. Berechnung des neuen Punktes
---
Der neue Punkt $\mathbf{v}'$ nach der Drehung ist:
## 2. Rotationsmatrizen im 3D-Raum
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}
1 & 0 & 0 \\
0 & \cos\theta & -\sin\theta \\
0 & \sin\theta & \cos\theta
\end{pmatrix}
\begin{pmatrix} x \\ y \\ z \end{pmatrix} =
\begin{pmatrix}
x \\
y \cos\theta - z \sin\theta \\
y \sin\theta + z \cos\theta
\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}
$$
Dann gilt:
$$
\cos \theta = 0, \quad \sin \theta = 1
$$
$$
\mathbf{A}' =
\mathbf{v}' = R_x(\theta)\mathbf{v} =
\begin{pmatrix}
1 \\
1 \cdot 0 - 1 \cdot 1 \\
1 \cdot 1 + 1 \cdot 0
\end{pmatrix} =
x \\
y \cos\theta - z \sin\theta \\
y \sin\theta + z \cos\theta
\end{pmatrix}
$$
---
### b) Rotation um die y-Achse
$$
R_y(\theta) =
\begin{pmatrix}
1 \\ -1 \\ 1
\cos\theta & 0 & \sin\theta \\
0 & 1 & 0 \\
-\sin\theta & 0 & \cos\theta
\end{pmatrix}
$$
## 5. Tabelle aller Punkte nach Rotation
Dabei bleibt die y-Koordinate gleich, x und z verändern sich.
$$
\begin{array}{c|c}
\text{Originalpunkt} & \text{Punkt nach Rotation} \\
\hline
A (1,1,1) & (1,-1,1) \\
B (1,1,-1) & (1,-1,-1) \\
C (1,-1,1) & (1,-1,-1) \\
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}
\mathbf{v}' = R_y(\theta)\mathbf{v} =
\begin{pmatrix}
x \cos\theta + z \sin\theta \\
y \\
- x \sin\theta + z \cos\theta
\end{pmatrix}
$$
## Fazit
---
- x bleibt unverändert
- y und z ändern sich je nach Winkel
- Rotationsmatrizen sind ein mächtiges Werkzeug, um Objekte im 3D-Raum zu bewegen
### c) Rotation um die z-Achse
$$
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
import mimetypes
import json
from jsmin import jsmin # pip install jsmin
from jsmin import jsmin
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 lua import plugin_manager
@@ -14,13 +22,253 @@ from PyPost import extract_summary
logger = Logger()
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__))
HTML_DIR = os.path.join(PROJECT_ROOT, "html")
MARKDOWN_DIR = os.path.join(PROJECT_ROOT, "markdown")
BASE_FILE = os.path.join(HTML_DIR, "base", "index.html")
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):
html_files = []
@@ -30,8 +278,17 @@ def get_html_files(directory=HTML_DIR):
html_files.append(entry)
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:
base_html = f.read()
@@ -65,9 +322,13 @@ def build_index_page() -> str:
continue
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 random
@@ -79,13 +340,9 @@ H1 = random.choice(hash_list)
H2_CANDIDATES = [h for h in hash_list if h != 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:
tor_link = "http://7uhuxits7qfmiagkmpazxvh3rtk6aijs6pbawge3fl77y4xqjixlhkqd.onion/"
return f"""
<!-- Footer styling doesnt need to work with
flex, or anything else, because pagnation.
-->
<div class="footer">
<footer>
<p>
@@ -108,10 +365,41 @@ def index_footer() -> str:
"""
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):
"""Parse POST request body"""
import json
content_length = int(self.headers.get('Content-Length', 0))
if content_length == 0:
return {}
@@ -133,13 +421,11 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
return {"raw": post_data}
def do_POST(self):
"""Handle POST requests - primarily for plugin routes"""
"""Handle POST for Plugins"""
req_path = self.path.lstrip("/")
# Parse POST data
post_data = self._parse_post_data()
# Add additional request info
request_data = {
"path": self.path,
"headers": dict(self.headers),
@@ -147,7 +433,6 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
"method": "POST"
}
# Check plugin routes
plugin_result = plugin_manager.handle_request("/" + req_path, request_data, method="POST")
if plugin_result is not None:
status, headers, body = plugin_result
@@ -164,7 +449,6 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
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()
@@ -172,18 +456,61 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
self.wfile.write(error_response.encode("utf-8"))
def do_GET(self):
req_path = self.path.lstrip("/") # normalize leading /
req_path = self.path.lstrip("/")
# Handle root/index
# Handle root/index with caching
if req_path == "" or req_path == "index.html":
content = build_index_page()
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(content.encode("utf-8"))
self._send_compressed_response(
content.encode("utf-8"),
"text/html",
"public, max-age=300" # Cache for 5 minutes
)
return
# CHECK PLUGIN ROUTES FIRST
# 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})
if plugin_result is not None:
status, headers, body = plugin_result
@@ -196,9 +523,8 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
# Handle markdown file downloads
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:
self.send_response(403)
self.end_headers()
@@ -225,11 +551,7 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
with open(markdown_file_path, "rb") as f:
content = f.read()
self.send_response(200)
self.send_header("Content-type", "text/markdown")
self.send_header("Content-Disposition", f'attachment; filename="{markdown_filename}"')
self.end_headers()
self.wfile.write(content)
self._send_compressed_response(content, "text/markdown")
logger.log_info(f"Served markdown file: {markdown_filename}")
return
@@ -242,9 +564,8 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
# Handle Lua file downloads
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:
self.send_response(403)
self.end_headers()
@@ -271,11 +592,7 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
with open(lua_file_path, "rb") as f:
content = f.read()
self.send_response(200)
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)
self._send_compressed_response(content, "text/x-lua")
logger.log_info(f"Served Lua file: {lua_filename}")
return
@@ -286,7 +603,7 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
self.wfile.write(b"500 - Internal Server Error")
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))
if not file_path.startswith(PROJECT_ROOT):
self.send_response(403)
@@ -295,6 +612,18 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
return
if os.path.isfile(file_path):
try:
if should_cache_file(file_path):
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)
self._send_compressed_response(
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"
@@ -302,17 +631,21 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
with open(file_path, "rb") as f:
content = f.read()
# Obfuscate JS on the fly
if 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}")
self.send_response(200)
self.send_header("Content-type", mime_type)
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(content)
self.wfile.write(b"500 - Internal Server Error")
return
self.send_response(404)
@@ -327,13 +660,25 @@ def run_pypost():
if __name__ == "__main__":
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()
logger.log_debug("Started PyPost.py in background watcher thread.")
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"Icon max size: {ICON_MAX_SIZE} \n Image max size: {STANDARD_IMAGE_MAX_SIZE}")
httpd.serve_forever()
prewarm_image_cache()
except (Exception, KeyboardInterrupt) as 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()