Compare commits
5 Commits
a74d482d7a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c170a195e | |||
| cee104f1f5 | |||
| ccabb5395c | |||
| a7847f6bff | |||
| 3494298330 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@ env
|
|||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
.idea
|
.idea
|
||||||
|
cdn
|
||||||
|
cache
|
||||||
|
|||||||
27
css/main.css
27
css/main.css
@@ -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
3
css/prism.css
Normal 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}
|
||||||
@@ -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."
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
@@ -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]
|
||||||
@@ -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>
|
|
||||||
<li>It is essential for downloading Markdown-Source</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|
||||||
<p id="available">
|
<p id="available" style="display:flex;align-items:center;gap:8px;">
|
||||||
<img src="../../css/icons/available.webp" width="40" height="40" alt="Available icon" />
|
<img src="/css/icons/available.webp" width="40" height="40" alt="Available" loading="lazy">
|
||||||
Available pages:
|
<span>Available pages:</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- CONTENT -->
|
<!-- CONTENT -->
|
||||||
|
|
||||||
|
<!-- Core scripts: async load -->
|
||||||
<!-- load scripts needed for indexer -->
|
<script src="/js/normal.js" defer></script>
|
||||||
<script src="../../js/normal.js"></script>
|
<script src="/js/search.js" defer></script>
|
||||||
<script src="../../js/search.js" defer></script>
|
<script src="/js/ulorder.js" defer></script>
|
||||||
<script src="../../js/ulorder.js" defer></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|||||||
@@ -4,67 +4,79 @@
|
|||||||
<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>
|
||||||
|
<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;">
|
<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>
|
{{ title }} <noscript>(Enable JavaScript!)</noscript>
|
||||||
</h1>
|
</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();">
|
<div class="meta" style="display:inline;cursor:pointer;" onclick="toggleDarkMode();">
|
||||||
Written @{{ now }}
|
Written @{{ now }}
|
||||||
</div>
|
</div>
|
||||||
<hr style="margin:10px 0;" />
|
<hr style="margin:10px 0;">
|
||||||
<div class="html-content">
|
<div class="html-content">
|
||||||
{{ html_body | safe }}
|
{{ html_body | safe }}
|
||||||
</div>
|
</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
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
32
js/post/sw.js
Normal 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;
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -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:**
|
Eine Drehung des Würfels im Raum kann mathematisch durch **Multiplikation mit einer Rotationsmatrix** beschrieben werden.
|
||||||
- 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:
|
## 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} =
|
\mathbf{v}' = R_x(\theta)\mathbf{v} =
|
||||||
\begin{pmatrix}
|
\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 \\
|
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}$.
|
||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
431
webserver.py
431
webserver.py
@@ -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,10 +365,41 @@ 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"""
|
|
||||||
import json
|
|
||||||
content_length = int(self.headers.get('Content-Length', 0))
|
content_length = int(self.headers.get('Content-Length', 0))
|
||||||
if content_length == 0:
|
if content_length == 0:
|
||||||
return {}
|
return {}
|
||||||
@@ -133,13 +421,11 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
return {"raw": post_data}
|
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),
|
||||||
@@ -147,7 +433,6 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
"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
|
||||||
@@ -164,7 +449,6 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(str(body).encode("utf-8"))
|
self.wfile.write(str(body).encode("utf-8"))
|
||||||
return
|
return
|
||||||
|
|
||||||
# No plugin handled this POST request
|
|
||||||
self.send_response(404)
|
self.send_response(404)
|
||||||
self.send_header("Content-type", "application/json")
|
self.send_header("Content-type", "application/json")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
@@ -172,18 +456,61 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(error_response.encode("utf-8"))
|
self.wfile.write(error_response.encode("utf-8"))
|
||||||
|
|
||||||
def do_GET(self):
|
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":
|
if req_path == "" or req_path == "index.html":
|
||||||
content = build_index_page()
|
content = build_index_page()
|
||||||
self.send_response(200)
|
self._send_compressed_response(
|
||||||
self.send_header("Content-type", "text/html")
|
content.encode("utf-8"),
|
||||||
self.end_headers()
|
"text/html",
|
||||||
self.wfile.write(content.encode("utf-8"))
|
"public, max-age=300" # Cache for 5 minutes
|
||||||
|
)
|
||||||
return
|
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})
|
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,6 +612,18 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
return
|
return
|
||||||
|
|
||||||
if os.path.isfile(file_path):
|
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)
|
mime_type, _ = mimetypes.guess_type(file_path)
|
||||||
if mime_type is None:
|
if mime_type is None:
|
||||||
mime_type = "application/octet-stream"
|
mime_type = "application/octet-stream"
|
||||||
@@ -302,17 +631,21 @@ class WebServerHTTPRequestHandler(BaseHTTPRequestHandler):
|
|||||||
with open(file_path, "rb") as f:
|
with open(file_path, "rb") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
# Obfuscate JS on the fly
|
|
||||||
if mime_type == "application/javascript" or file_path.endswith(".js"):
|
if mime_type == "application/javascript" or file_path.endswith(".js"):
|
||||||
try:
|
try:
|
||||||
content = jsmin(content.decode("utf-8")).encode("utf-8")
|
content = jsmin(content.decode("utf-8")).encode("utf-8")
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.log_error(f"Error minifying JS file {file_path}: {err}")
|
logger.log_error(f"Error minifying JS file {file_path}: {err}")
|
||||||
|
|
||||||
self.send_response(200)
|
self._send_compressed_response(content, mime_type)
|
||||||
self.send_header("Content-type", 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.end_headers()
|
||||||
self.wfile.write(content)
|
self.wfile.write(b"500 - Internal Server Error")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.send_response(404)
|
self.send_response(404)
|
||||||
@@ -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:
|
||||||
|
if KeyboardInterrupt:
|
||||||
|
logger.log_info(f"Shutting down server.\n Reason: KeyboardInterrupt")
|
||||||
|
else:
|
||||||
logger.log_info(f"Shutting down server.\n Reason: {e}")
|
logger.log_info(f"Shutting down server.\n Reason: {e}")
|
||||||
httpd.server_close()
|
httpd.server_close()
|
||||||
Reference in New Issue
Block a user