chore(website): simplify maintenance and detect documentation drift
This commit is contained in:
parent
8c7569aca7
commit
5aa30b8b86
|
|
@ -35,9 +35,5 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Check sitemap discovery and metadata regressions
|
||||
run: python3 scripts/test-website.py
|
||||
- name: Check shared HTML, links, assets and contributor workflows
|
||||
run: python3 scripts/website.py check
|
||||
|
|
|
|||
|
|
@ -59,10 +59,8 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Check shared HTML, links, assets and contributor workflows
|
||||
run: python3 scripts/website.py check
|
||||
- name: Record the published commit
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,135 @@
|
|||
"""Optionally audit external website links; network failures stay distinct from missing targets."""
|
||||
|
||||
import argparse
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from functools import lru_cache
|
||||
from html.parser import HTMLParser
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.error import HTTPError, URLError
|
||||
from urllib.parse import quote, unquote, urlsplit
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
from website import authored_markdown
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def github_api(endpoint):
|
||||
"""Read GitHub evidence through the caller's gh installation and authentication."""
|
||||
result = subprocess.run(["gh", "api", endpoint], capture_output=True, text=True, timeout=20)
|
||||
if result.returncode:
|
||||
raise ValueError(result.stderr.strip())
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def github_target(url):
|
||||
"""Resolve repository URLs, including branch names containing slashes."""
|
||||
parts = [unquote(part) for part in url.path.strip("/").split("/")]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
repo = f"repos/{parts[0]}/{parts[1]}"
|
||||
if url.hostname == "raw.githubusercontent.com" and len(parts) >= 4:
|
||||
return f"{repo}/contents/{'/'.join(parts[3:])}?ref={quote(parts[2], safe='')}"
|
||||
if len(parts) == 2:
|
||||
return repo
|
||||
kind = parts[2]
|
||||
if kind in ("blob", "tree") and len(parts) >= 4:
|
||||
ref = parts[3]
|
||||
# GitHub uses the longest matching branch name before the source path.
|
||||
if not re.fullmatch(r"[0-9a-f]{40}", ref):
|
||||
references = github_api(f"{repo}/git/matching-refs/heads/{quote(ref, safe='')}")
|
||||
references += github_api(f"{repo}/git/matching-refs/tags/{quote(ref, safe='')}")
|
||||
suffix = "/".join(parts[3:])
|
||||
matches = [item["ref"].split("/", 2)[2] for item in references]
|
||||
matches = [name for name in matches if suffix == name or suffix.startswith(name + "/")]
|
||||
if matches:
|
||||
ref = max(matches, key=len)
|
||||
path = "/".join(parts[3:])[len(ref):].lstrip("/")
|
||||
return f"{repo}/contents/{quote(path, safe='/')}?ref={quote(ref, safe='')}"
|
||||
if kind in ("issues", "pull") and len(parts) == 4 and parts[3].isdigit():
|
||||
return f"{repo}/{'pulls' if kind == 'pull' else 'issues'}/{parts[3]}"
|
||||
if kind == "commit" and len(parts) == 4:
|
||||
return f"{repo}/commits/{parts[3]}"
|
||||
if kind == "releases" and len(parts) >= 5 and parts[3] in ("tag", "download"):
|
||||
return f"{repo}/releases/tags/{quote(parts[4], safe='')}"
|
||||
return None
|
||||
|
||||
|
||||
def audit_url(item):
|
||||
"""Classify confirmed missing links separately from blocked or unsupported probes."""
|
||||
value, sources = item
|
||||
result = {"url": value, "sources": sorted(set(sources))}
|
||||
url = urlsplit(value)
|
||||
try:
|
||||
if url.hostname in ("github.com", "raw.githubusercontent.com"):
|
||||
endpoint = github_target(url)
|
||||
if endpoint is None:
|
||||
return dict(result, status="unknown", reason="GitHub UI route; check in a browser")
|
||||
data = github_api(endpoint)
|
||||
parts = url.path.strip("/").split("/")
|
||||
if len(parts) >= 6 and parts[2:4] == ["releases", "download"]:
|
||||
if unquote(parts[5]) not in [asset["name"] for asset in data.get("assets", [])]:
|
||||
return dict(result, status="broken", reason="Release asset is missing")
|
||||
return dict(result, status="ok", via="gh api", endpoint=endpoint)
|
||||
request = Request(value.split("#", 1)[0], headers={
|
||||
"User-Agent": "REA-Website-Verifier/1.0 (+https://github.com/morluto/rea)",
|
||||
})
|
||||
with urlopen(request, timeout=15) as response:
|
||||
return dict(result, status="ok", http_status=response.status, final_url=response.url)
|
||||
except HTTPError as error:
|
||||
return dict(result, status="broken" if error.code in (404, 410) else "unknown", reason=str(error))
|
||||
except ValueError as error:
|
||||
return dict(result, status="broken" if "HTTP 404" in str(error) else "unknown", reason=str(error))
|
||||
except (OSError, URLError, TimeoutError, subprocess.TimeoutExpired) as error:
|
||||
return dict(result, status="unknown", reason=str(error))
|
||||
|
||||
|
||||
def external_links(root):
|
||||
"""Discover authored external links, excluding locally checked site and preview URLs."""
|
||||
references = {}
|
||||
|
||||
def add(value, source):
|
||||
url = urlsplit(value)
|
||||
if url.scheme in ("http", "https") and url.hostname not in ("rea.tools", "localhost", "127.0.0.1", "::1"):
|
||||
references.setdefault(value, []).append(str(source.relative_to(root)))
|
||||
|
||||
class Links(HTMLParser):
|
||||
def handle_starttag(self, tag, attrs):
|
||||
for key, value in attrs:
|
||||
if key in ("href", "src") and value:
|
||||
add(value, path)
|
||||
|
||||
for path in sorted((root / "website/public").rglob("*.html")):
|
||||
Links().feed(path.read_text(encoding="utf-8"))
|
||||
for path in authored_markdown(root):
|
||||
for value in re.findall(r"(?:\]\(|<)(https?://[^\s<>\)]+)", path.read_text(encoding="utf-8")):
|
||||
add(value, path)
|
||||
return references
|
||||
|
||||
|
||||
def main():
|
||||
"""Run an explicit network audit without making PR checks depend on third-party uptime."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--report", type=Path, help="Write the complete JSON report outside public assets")
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
results = list(pool.map(audit_url, external_links(root).items()))
|
||||
if args.report:
|
||||
if args.report.resolve().is_relative_to(root / "website/public"):
|
||||
parser.error("Keep audit reports outside website/public.")
|
||||
args.report.write_text(json.dumps(results, indent=2) + "\n", encoding="utf-8")
|
||||
counts = {status: sum(item["status"] == status for item in results) for status in ("ok", "broken", "unknown")}
|
||||
print(f"External links: {counts['ok']} reachable, {counts['broken']} missing, {counts['unknown']} need manual verification.")
|
||||
print("GitHub checks establish target existence; external fragment IDs are not checked.")
|
||||
for item in results:
|
||||
if item["status"] != "ok":
|
||||
print(f"{item['status']}: {item['url']} — {item['reason']}")
|
||||
return 1 if counts["broken"] else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
"""Prepare downloads, the sitemap and the sharing image for the static website."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
from website import PageMetadata, page_url
|
||||
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
EXAMPLE_FILES = (
|
||||
"package.json",
|
||||
"main.js",
|
||||
|
|
@ -21,47 +19,20 @@ EXAMPLE_FILES = (
|
|||
)
|
||||
|
||||
|
||||
class IndexPolicy(HTMLParser):
|
||||
"""Read indexing directives from the authored HTML head."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.in_head = False
|
||||
self.noindex = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "head":
|
||||
self.in_head = True
|
||||
if tag == "meta" and self.in_head:
|
||||
attributes = dict(attrs)
|
||||
if (attributes.get("name") or "").lower() in ("robots", "googlebot"):
|
||||
directives = re.split(
|
||||
r"[\s,]+", (attributes.get("content") or "").lower()
|
||||
)
|
||||
self.noindex |= bool(set(directives) & {"noindex", "none"})
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
self.in_head = False
|
||||
|
||||
|
||||
def prepare_sitemap(site):
|
||||
"""Discover HTML routes automatically, excluding pages marked noindex."""
|
||||
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
ET.register_namespace("", namespace)
|
||||
sitemap = ET.Element(f"{{{namespace}}}urlset")
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
policy = IndexPolicy()
|
||||
policy = PageMetadata()
|
||||
policy.feed(path.read_text(encoding="utf-8"))
|
||||
if policy.noindex:
|
||||
continue
|
||||
route = path.relative_to(site).as_posix()
|
||||
if path.name == "index.html":
|
||||
route = route.removesuffix("index.html")
|
||||
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
|
||||
ET.SubElement(
|
||||
entry, f"{{{namespace}}}loc"
|
||||
).text = f"{SITE_ORIGIN}/{quote(route, safe='/')}"
|
||||
).text = page_url(site, path)
|
||||
ET.indent(sitemap, space=" ")
|
||||
ET.ElementTree(sitemap).write(
|
||||
site / "sitemap.xml", encoding="utf-8", xml_declaration=True
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import io
|
|||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
from threading import Thread
|
||||
import unittest
|
||||
from http.server import ThreadingHTTPServer
|
||||
from urllib.error import HTTPError
|
||||
from urllib.request import urlopen
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
|
|
@ -23,6 +27,7 @@ def load_script(name):
|
|||
|
||||
PREPARE = load_script("prepare-website")
|
||||
VERIFY = load_script("verify-website")
|
||||
WEBSITE = load_script("website")
|
||||
|
||||
|
||||
class WebsiteSearchChecks(unittest.TestCase):
|
||||
|
|
@ -126,5 +131,116 @@ class WebsiteSearchChecks(unittest.TestCase):
|
|||
)
|
||||
|
||||
|
||||
class WebsiteMaintenanceChecks(unittest.TestCase):
|
||||
"""Exercise contributor edits and the static preview through real consumers."""
|
||||
|
||||
def test_shared_edits_preserve_body_and_catch_drift_before_rewriting(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in ("public", "templates"):
|
||||
shutil.copytree(ROOT / "website" / name, root / "website" / name)
|
||||
page = root / "website/public/blog/touhou-reconstruction/index.html"
|
||||
original = page.read_text()
|
||||
body = original.split('<main id="main"', 1)[1].split("</main>", 1)[0]
|
||||
template = root / "website/templates/header.html"
|
||||
template.write_text(template.read_text().replace("Reverse Engineer Anything", "REA test navigation"))
|
||||
with self.assertRaisesRegex(ValueError, "Shared HTML is stale"):
|
||||
WEBSITE.sync_pages(root, check=True)
|
||||
self.assertEqual(page.read_text(), original)
|
||||
WEBSITE.sync_pages(root)
|
||||
updated = page.read_text()
|
||||
self.assertIn("REA test navigation", updated)
|
||||
self.assertEqual(updated.split('<main id="main"', 1)[1].split("</main>", 1)[0], body)
|
||||
self.assertEqual(WEBSITE.sync_pages(root), [])
|
||||
page.write_text(updated.replace(' <!-- website:header:end -->', '', 1))
|
||||
with self.assertRaisesRegex(ValueError, "needs one header region"):
|
||||
WEBSITE.sync_pages(root)
|
||||
|
||||
def test_new_page_escapes_metadata_stays_a_draft_and_joins_sitemap_when_ready(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for name in ("public", "templates"):
|
||||
shutil.copytree(ROOT / "website" / name, root / "website" / name)
|
||||
site = root / "website/public"
|
||||
page = WEBSITE.new_page(root, "blog/new-example", 'A <test> & "result"', 'Check "quotes" & <markup>.')
|
||||
source = page.read_text()
|
||||
self.assertIn('href="../../blog/" aria-current="page"', source)
|
||||
self.assertIn('href="../../assets/styles.css"', source)
|
||||
self.assertIn("A <test> & "result"", source)
|
||||
with self.assertRaisesRegex(ValueError, "Page already exists"):
|
||||
WEBSITE.new_page(root, "blog/new-example", "Another page", "Description")
|
||||
with self.assertRaises(ValueError):
|
||||
WEBSITE.new_page(root, "../outside", "Title", "Description")
|
||||
WEBSITE.sync_pages(root, check=True)
|
||||
PREPARE.prepare_sitemap(site)
|
||||
self.assertNotIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
|
||||
page.write_text(source.replace(' <meta name="robots" content="noindex" />\n', ''))
|
||||
PREPARE.prepare_sitemap(site)
|
||||
self.assertIn("https://rea.tools/blog/new-example/", (site / "sitemap.xml").read_text())
|
||||
self.assertEqual(VERIFY.check_site(site)[3], [])
|
||||
|
||||
def test_links_in_prompts_fragments_and_copy_controls_are_verified(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory) / "public"
|
||||
shutil.copytree(ROOT / "website/public", site)
|
||||
page = site / "faq/index.html"
|
||||
original = page.read_text()
|
||||
cases = (
|
||||
('<a href="https://rea.tools/deleted/">Broken</a>', "missing local target"),
|
||||
('<code>https://rea.tools/guides/native/#deleted</code>', "missing HTML fragment"),
|
||||
('<code>https://morluto.github.io/rea/examples/notes-web/</code>', "obsolete website URL"),
|
||||
('<button data-copy="deleted">Copy</button>', "missing copy target"),
|
||||
('<a href="https://[">Malformed</a>', "invalid URL"),
|
||||
)
|
||||
for inserted, message in cases:
|
||||
with self.subTest(message=message):
|
||||
page.write_text(original.replace("</main>", inserted + "</main>", 1))
|
||||
self.assertTrue(any(message in error for error in VERIFY.check_site(site)[3]))
|
||||
|
||||
def test_repository_links_reject_missing_files_and_old_markdown_anchors(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "website/public").mkdir(parents=True)
|
||||
(root / "docs").mkdir()
|
||||
(root / "docs/guide.md").write_text("# Guide\n## Set up REA\n")
|
||||
page = root / "website/README.md"
|
||||
page.write_text("[Guide](../docs/guide.md#set-up-rea)\n")
|
||||
(root / "website/.venv").mkdir()
|
||||
(root / "website/.venv/vendor.md").write_text("[File](missing.md)")
|
||||
self.assertEqual(VERIFY.check_repository_links(root), [])
|
||||
page.write_text("[Guide](../docs/guide.md#removed)\n[File](../docs/removed.md)\n")
|
||||
errors = VERIFY.check_repository_links(root)
|
||||
self.assertTrue(any("missing Markdown fragment" in error for error in errors))
|
||||
self.assertTrue(any("missing repository target" in error for error in errors))
|
||||
|
||||
def test_preview_preserves_pages_prefix_for_assets_and_directory_redirects(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory)
|
||||
(site / "guide").mkdir()
|
||||
(site / "guide/index.html").write_text("<h1>Guide</h1>")
|
||||
(site / "asset.css").write_text("body { color: black; }")
|
||||
for prefix in ("/", "/rea/"):
|
||||
with self.subTest(prefix=prefix), ThreadingHTTPServer(("127.0.0.1", 0), WEBSITE.preview_handler(site, prefix)) as server:
|
||||
worker = Thread(target=server.serve_forever, daemon=True)
|
||||
worker.start()
|
||||
try:
|
||||
base = f"http://127.0.0.1:{server.server_port}"
|
||||
with urlopen(base + prefix + "guide?test=1") as response:
|
||||
self.assertEqual(response.url, base + prefix + "guide/?test=1")
|
||||
self.assertEqual(response.read(), b"<h1>Guide</h1>")
|
||||
with urlopen(base + prefix + "asset.css") as response:
|
||||
self.assertIn(b"color: black", response.read())
|
||||
with self.assertRaises(HTTPError) as error:
|
||||
urlopen(base + prefix + "missing")
|
||||
self.assertEqual(error.exception.code, 404)
|
||||
if prefix != "/":
|
||||
with self.assertRaises(HTTPError) as error:
|
||||
urlopen(base + "/guide/")
|
||||
self.assertEqual(error.exception.code, 404)
|
||||
finally:
|
||||
server.shutdown()
|
||||
worker.join()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
|
|||
|
|
@ -5,13 +5,79 @@ from pathlib import Path
|
|||
import re
|
||||
import struct
|
||||
import sys
|
||||
from urllib.parse import quote, unquote, urlsplit
|
||||
import unicodedata
|
||||
from urllib.parse import unquote, urlsplit
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
from website import NOTES_FIXTURES, SITE_ORIGIN, authored_markdown, page_url, sync_pages
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
|
||||
LEGACY_ORIGIN = "morluto.github.io"
|
||||
SITE_TEXT_URL = re.compile(
|
||||
r"https?://(?:rea\.tools|morluto\.github\.io)/[^\s<>\"'`]+", re.IGNORECASE
|
||||
)
|
||||
|
||||
|
||||
def markdown_ids(path):
|
||||
"""Collect GitHub heading anchors and explicit HTML IDs in local Markdown."""
|
||||
source = path.read_text(encoding="utf-8")
|
||||
parser = HtmlReferences()
|
||||
parser.feed(source)
|
||||
identifiers = set(parser.ids)
|
||||
counts = {}
|
||||
fenced = False
|
||||
for line in source.splitlines():
|
||||
if line.lstrip().startswith(("```", "~~~")):
|
||||
fenced = not fenced
|
||||
if fenced:
|
||||
continue
|
||||
heading = re.match(r"^ {0,3}#{1,6}\s+(.+?)(?:\s+#+)?\s*$", line)
|
||||
if not heading:
|
||||
continue
|
||||
text = re.sub(r"<[^>]*>", "", heading[1]).lower()
|
||||
slug = "".join(c for c in text if c in "-_ " or unicodedata.category(c)[0] in "LN").replace(" ", "-")
|
||||
count = counts.get(slug, 0)
|
||||
identifiers.add(f"{slug}-{count}" if count else slug)
|
||||
counts[slug] = count + 1
|
||||
return identifiers
|
||||
|
||||
|
||||
def check_repository_links(root):
|
||||
"""Check local links in website Markdown and current-repository source links."""
|
||||
errors = []
|
||||
sources = {}
|
||||
for path in authored_markdown(root):
|
||||
text = re.sub(r"(?ms)^\s*(```|~~~).*?^\s*\1[^\n]*$", "", path.read_text(encoding="utf-8"))
|
||||
text = re.sub(r"`[^`]*`", "", text)
|
||||
sources[path] = re.findall(r"\[[^\]\n]*\]\(([^\s)]+)\)", text)
|
||||
for path in sorted((root / "website/public").rglob("*.html")):
|
||||
parser = HtmlReferences()
|
||||
parser.feed(path.read_text(encoding="utf-8"))
|
||||
sources[path] = parser.references
|
||||
for path, references in sources.items():
|
||||
for reference in references:
|
||||
try:
|
||||
url = urlsplit(reference)
|
||||
except ValueError as error:
|
||||
errors.append(f"{path.relative_to(root)}: invalid URL {reference!r}: {error}")
|
||||
continue
|
||||
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
|
||||
errors.append(f"{path.relative_to(root)}: obsolete website URL: {reference}")
|
||||
continue
|
||||
if url.hostname == "github.com":
|
||||
match = re.match(r"^/morluto/rea/(?:blob|tree)/main/(.+)$", url.path)
|
||||
if not match:
|
||||
continue
|
||||
target = (root / unquote(match[1])).resolve()
|
||||
elif url.scheme or url.netloc or path.suffix == ".html":
|
||||
continue
|
||||
else:
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
if not target.is_relative_to(root) or not target.exists():
|
||||
errors.append(f"{path.relative_to(root)}: missing repository target: {reference}")
|
||||
elif target.suffix == ".md" and url.fragment and unquote(url.fragment) not in markdown_ids(target):
|
||||
errors.append(f"{path.relative_to(root)}: missing Markdown fragment: {reference}")
|
||||
return errors
|
||||
|
||||
|
||||
class HtmlReferences(HTMLParser):
|
||||
|
|
@ -30,6 +96,8 @@ class HtmlReferences(HTMLParser):
|
|||
self.canonicals = []
|
||||
self.lang = None
|
||||
self.h1_count = 0
|
||||
self.text = []
|
||||
self.copy_targets = []
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attributes = dict(attrs)
|
||||
|
|
@ -67,6 +135,8 @@ class HtmlReferences(HTMLParser):
|
|||
reference = attributes.get(attribute)
|
||||
if reference is not None:
|
||||
self.references.append(reference)
|
||||
if attributes.get("data-copy"):
|
||||
self.copy_targets.append(attributes["data-copy"])
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
|
|
@ -75,6 +145,7 @@ class HtmlReferences(HTMLParser):
|
|||
self.in_title = False
|
||||
|
||||
def handle_data(self, data):
|
||||
self.text.append(data)
|
||||
if self.in_title:
|
||||
self.titles[-1] += data
|
||||
|
||||
|
|
@ -104,15 +175,7 @@ def check_search_metadata(site, pages):
|
|||
)
|
||||
if page.noindex():
|
||||
continue
|
||||
route = (
|
||||
path.relative_to(site).parent.as_posix()
|
||||
if path.name == "index.html"
|
||||
else label
|
||||
)
|
||||
route = "" if route == "." else route
|
||||
url = f"{SITE_ORIGIN}/{quote(route, safe='/')}" + (
|
||||
"/" if route and path.name == "index.html" else ""
|
||||
)
|
||||
url = page_url(site, path)
|
||||
indexable.add(url)
|
||||
if page.canonicals != [url]:
|
||||
errors.append(f"{label}: needs exactly one head canonical for {url}")
|
||||
|
|
@ -233,15 +296,32 @@ def check_site(site):
|
|||
label = path.relative_to(site)
|
||||
for identifier in page.duplicate_ids:
|
||||
errors.append(f"{label}: duplicate ID {identifier!r}")
|
||||
for reference in page.references:
|
||||
url = urlsplit(reference)
|
||||
if url.scheme or url.netloc:
|
||||
for identifier in page.copy_targets:
|
||||
if identifier not in page.ids:
|
||||
errors.append(f"{label}: missing copy target: {identifier}")
|
||||
text_urls = [match.group().rstrip(".,;)") for match in SITE_TEXT_URL.finditer(" ".join(page.text))]
|
||||
for reference in page.references + text_urls:
|
||||
try:
|
||||
url = urlsplit(reference)
|
||||
except ValueError as error:
|
||||
errors.append(f"{label}: invalid URL {reference!r}: {error}")
|
||||
continue
|
||||
references += 1
|
||||
if url.path.startswith("/"):
|
||||
if url.hostname == LEGACY_ORIGIN and (url.path == "/rea" or url.path.startswith("/rea/")):
|
||||
errors.append(f"{label}: obsolete website URL: {reference}; use rea.tools")
|
||||
continue
|
||||
if url.hostname == "rea.tools":
|
||||
if url.scheme != "https" or url.netloc != "rea.tools":
|
||||
errors.append(f"{label}: website URL must use {SITE_ORIGIN}: {reference}")
|
||||
continue
|
||||
target = (site / unquote(url.path).lstrip("/")).resolve()
|
||||
elif url.scheme or url.netloc:
|
||||
continue
|
||||
elif url.path.startswith("/"):
|
||||
errors.append(f"{label}: use a relative site reference: {reference}")
|
||||
continue
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
else:
|
||||
target = (path.parent / unquote(url.path)).resolve() if url.path else path
|
||||
references += 1
|
||||
if not target.is_relative_to(site):
|
||||
errors.append(f"{label}: reference leaves the website: {reference}")
|
||||
continue
|
||||
|
|
@ -323,6 +403,11 @@ def main():
|
|||
pages, references, indexable, errors = check_site(root / "website/public")
|
||||
errors.extend(check_example_archive(root / "website/public"))
|
||||
errors.extend(check_publisher(root))
|
||||
errors.extend(check_repository_links(root))
|
||||
try:
|
||||
sync_pages(root, check=True)
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
errors.append(str(error))
|
||||
if errors:
|
||||
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -0,0 +1,245 @@
|
|||
"""Maintain shared HTML and run the static website's contributor commands."""
|
||||
|
||||
import argparse
|
||||
from functools import partial
|
||||
from html import escape
|
||||
from html.parser import HTMLParser
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path, PurePosixPath
|
||||
from string import Template
|
||||
import subprocess
|
||||
import sys
|
||||
from urllib.parse import quote, urlsplit
|
||||
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
|
||||
NAVIGATION = (
|
||||
("Showcases", "showcase/"),
|
||||
("Blog", "blog/"),
|
||||
("Get started", "get-started/"),
|
||||
("Guides", "guides/"),
|
||||
("FAQ", "faq/"),
|
||||
)
|
||||
|
||||
|
||||
def authored_markdown(root):
|
||||
"""Discover website documentation while excluding local environments and vendors."""
|
||||
site = root / "website"
|
||||
return [
|
||||
path for path in sorted(site.rglob("*.md"))
|
||||
if not any(part.startswith(".") or part in ("node_modules", "__pycache__")
|
||||
for part in path.relative_to(site).parts)
|
||||
]
|
||||
|
||||
|
||||
class PageMetadata(HTMLParser):
|
||||
"""Read the authored title, description and indexing policy from the head."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.in_head = False
|
||||
self.in_title = False
|
||||
self.titles = []
|
||||
self.descriptions = []
|
||||
self.noindex = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "head":
|
||||
self.in_head = True
|
||||
if not self.in_head:
|
||||
return
|
||||
if tag == "title":
|
||||
self.in_title = True
|
||||
self.titles.append("")
|
||||
if tag == "meta":
|
||||
attributes = dict(attrs)
|
||||
name = (attributes.get("name") or "").lower()
|
||||
content = attributes.get("content") or ""
|
||||
if name == "description":
|
||||
self.descriptions.append(content)
|
||||
if name in ("robots", "googlebot"):
|
||||
self.noindex |= bool(
|
||||
set(content.lower().replace(",", " ").split()) & {"noindex", "none"}
|
||||
)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
self.in_head = False
|
||||
if tag == "title":
|
||||
self.in_title = False
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.in_title:
|
||||
self.titles[-1] += data
|
||||
|
||||
|
||||
def page_route(site, path):
|
||||
"""Map an HTML file to its public route without a separate page registry."""
|
||||
route = path.relative_to(site).as_posix()
|
||||
return route.removesuffix("index.html") if path.name == "index.html" else route
|
||||
|
||||
|
||||
def page_url(site, path):
|
||||
"""Return the canonical public URL of an authored HTML file."""
|
||||
return f"{SITE_ORIGIN}/{quote(page_route(site, path), safe='/')}"
|
||||
|
||||
|
||||
def shared_blocks(root, path, source):
|
||||
"""Render shared regions while keeping the page's body and metadata authored."""
|
||||
site = root / "website/public"
|
||||
metadata = PageMetadata()
|
||||
metadata.feed(source)
|
||||
for name, values in (("title", metadata.titles), ("description", metadata.descriptions)):
|
||||
if len(values) != 1 or not values[0].strip():
|
||||
raise ValueError(f"{path.relative_to(site)}: needs one nonempty head {name}")
|
||||
depth = len(path.relative_to(site).parent.parts)
|
||||
base = "../" * depth or "./"
|
||||
route = page_route(site, path)
|
||||
links = []
|
||||
for label, destination in NAVIGATION:
|
||||
attributes = ' class="nav-start"' if destination == "get-started/" else ""
|
||||
if route.startswith(destination):
|
||||
attributes += ' aria-current="page"'
|
||||
links.append(f' <a href="{base}{destination}"{attributes}>{label}</a>')
|
||||
values = {
|
||||
"base": base,
|
||||
"navigation": "\n".join(links),
|
||||
"canonical": escape(page_url(site, path), quote=True),
|
||||
"origin": SITE_ORIGIN,
|
||||
"title": escape(metadata.titles[0].strip(), quote=True),
|
||||
"description": escape(metadata.descriptions[0].strip(), quote=True),
|
||||
}
|
||||
templates = root / "website/templates"
|
||||
return {
|
||||
name: Template((templates / f"{name}.html").read_text(encoding="utf-8")).substitute(values)
|
||||
for name in ("sharing", "header", "footer")
|
||||
}
|
||||
|
||||
|
||||
def sync_pages(root, check=False):
|
||||
"""Update shared regions, or reject drift without changing any authored file."""
|
||||
site = root / "website/public"
|
||||
changes = []
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
if path.relative_to(site).as_posix() in NOTES_FIXTURES:
|
||||
continue
|
||||
original = path.read_text(encoding="utf-8")
|
||||
source = original
|
||||
for name, block in shared_blocks(root, path, source).items():
|
||||
start = f" <!-- website:{name}:start -->\n"
|
||||
end = f" <!-- website:{name}:end -->"
|
||||
if source.count(start) != 1 or source.count(end) != 1:
|
||||
raise ValueError(f"{path.relative_to(site)}: needs one {name} region; use new-page for a new reading page")
|
||||
before, _, remaining = source.partition(start)
|
||||
_, found, after = remaining.partition(end)
|
||||
if not found:
|
||||
raise ValueError(f"{path.relative_to(site)}: {name} region is out of order")
|
||||
source = before + start + block + end + after
|
||||
if source != original:
|
||||
changes.append((path, source))
|
||||
if check and changes:
|
||||
names = ", ".join(str(path.relative_to(site)) for path, _ in changes)
|
||||
raise ValueError(f"Shared HTML is stale: {names}. Run python3 scripts/website.py sync and commit the updated pages.")
|
||||
if not check:
|
||||
for path, source in changes:
|
||||
path.write_text(source, encoding="utf-8")
|
||||
print(f"{'Checked' if check else 'Synced'} shared HTML ({len(changes)} pages changed).")
|
||||
return [path for path, _ in changes]
|
||||
|
||||
|
||||
def new_page(root, route, title, description):
|
||||
"""Create a draft reading page with correct depth, metadata and shared regions."""
|
||||
parts = PurePosixPath(route)
|
||||
if not route or parts.is_absolute() or ".." in parts.parts or "\\" in route or parts.suffix:
|
||||
raise ValueError("Use a relative directory route, such as blog/my-article.")
|
||||
site = root / "website/public"
|
||||
path = site.joinpath(*parts.parts, "index.html")
|
||||
if not path.resolve().is_relative_to(site.resolve()):
|
||||
raise ValueError("The new page must stay inside website/public.")
|
||||
if path.exists():
|
||||
raise ValueError(f"Page already exists: {path.relative_to(site)}")
|
||||
base = "../" * len(parts.parts) or "./"
|
||||
source = Template((root / "website/templates/page.html").read_text(encoding="utf-8")).substitute(
|
||||
title=escape(title, quote=True), description=escape(description, quote=True), base=base,
|
||||
)
|
||||
# Render before writing, so invalid input cannot leave an incomplete page.
|
||||
blocks = shared_blocks(root, path, source)
|
||||
for name, block in blocks.items():
|
||||
source = source.replace(f" <!-- website:{name}:start -->\n", f" <!-- website:{name}:start -->\n{block}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(source, encoding="utf-8")
|
||||
print(f"Created {path.relative_to(root)}. Write the page, add its index link, then remove the Draft label and noindex directive when ready.")
|
||||
return path
|
||||
|
||||
|
||||
def run_script(root, name, arguments=()):
|
||||
"""Use the active Python environment for the existing preparation and checks."""
|
||||
subprocess.run([sys.executable, str(root / "scripts" / name), *arguments], cwd=root, check=True)
|
||||
|
||||
|
||||
def preview_handler(site, prefix):
|
||||
"""Serve one static directory at root or a Pages prefix, preserving redirects."""
|
||||
class Handler(SimpleHTTPRequestHandler):
|
||||
def send_head(self):
|
||||
if not urlsplit(self.path).path.startswith(prefix):
|
||||
self.send_error(404)
|
||||
return None
|
||||
return super().send_head()
|
||||
|
||||
def translate_path(self, path):
|
||||
return super().translate_path("/" + path[len(prefix):])
|
||||
|
||||
return partial(Handler, directory=str(site))
|
||||
|
||||
|
||||
def main():
|
||||
"""Expose one small command for synchronizing, checking and previewing the site."""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
sync = commands.add_parser("sync", help="Update shared HTML after editing templates or page metadata")
|
||||
sync.add_argument("--check", action="store_true", help="Report drift without rewriting pages")
|
||||
commands.add_parser("check", help="Check shared HTML, prepare assets, verify links and run regression tests")
|
||||
links = commands.add_parser("links", help="Audit external targets using gh and HTTP; separate from offline checks")
|
||||
links.add_argument("--report", help="Write the full external-link JSON report")
|
||||
create = commands.add_parser("new-page", help="Create a draft reading page")
|
||||
create.add_argument("route")
|
||||
create.add_argument("--title", required=True)
|
||||
create.add_argument("--description", required=True)
|
||||
serve = commands.add_parser("serve", help="Prepare and preview the static site")
|
||||
serve.add_argument("--port", type=int, default=4173)
|
||||
serve.add_argument("--base-path", choices=("/", "/rea/"), default="/")
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
try:
|
||||
if args.command == "new-page":
|
||||
new_page(root, args.route, args.title, args.description)
|
||||
elif args.command == "sync":
|
||||
sync_pages(root, check=args.check)
|
||||
elif args.command == "check":
|
||||
sync_pages(root, check=True)
|
||||
for name in ("prepare-website.py", "verify-website.py", "test-website.py"):
|
||||
run_script(root, name)
|
||||
elif args.command == "links":
|
||||
run_script(root, "audit-website-links.py", ["--report", args.report] if args.report else [])
|
||||
else:
|
||||
sync_pages(root)
|
||||
run_script(root, "prepare-website.py")
|
||||
prefix = args.base_path
|
||||
|
||||
handler = preview_handler(root / "website/public", prefix)
|
||||
with ThreadingHTTPServer(("127.0.0.1", args.port), handler) as server:
|
||||
print(f"Preview: http://127.0.0.1:{server.server_port}{prefix}", flush=True)
|
||||
server.serve_forever()
|
||||
except (OSError, ValueError, KeyError) as error:
|
||||
print(str(error), file=sys.stderr)
|
||||
return 1
|
||||
except subprocess.CalledProcessError as error:
|
||||
return error.returncode
|
||||
except KeyboardInterrupt:
|
||||
return 0
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -12,6 +12,12 @@ for asset notes and the source of case-study claims.
|
|||
should explain a new fact or action.
|
||||
- Reuse the shared styles, prompts, figures and code-step components. Keep a
|
||||
case study focused on one question, with further examples in native details.
|
||||
- Edit shared header, footer and sharing markup in `templates/`; run
|
||||
`python3 scripts/website.py sync` and commit the rendered regions with their
|
||||
templates. Keep page bodies outside the marked regions. Author each title and
|
||||
description once; derive sharing fields and canonicals from them. Use
|
||||
`new-page` for a reading-page scaffold, then add its index link. Keep a draft's
|
||||
visible label and `noindex` until the content is ready.
|
||||
- Show what REA returns and how the agent uses it. Preserve source attribution,
|
||||
target identity and verification scope.
|
||||
- Blog articles teach methods, decisions and workflows through concrete cases.
|
||||
|
|
@ -27,9 +33,13 @@ for asset notes and the source of case-study claims.
|
|||
from HTML automatically. Mark example applications `noindex` in the head and
|
||||
keep them crawlable. Maintain the SVG sharing source, not the generated PNG.
|
||||
- Check both local root and `/rea/` paths, mobile layouts, expanded details,
|
||||
copying and downloads. Run `scripts/prepare-website.py` followed by
|
||||
`scripts/verify-website.py` and `scripts/test-website.py`, using the website
|
||||
Python environment described in the README.
|
||||
copying and downloads. Run `python3 scripts/website.py check`, using the Python
|
||||
environment described in the README. It rejects drift rather than silently
|
||||
rewriting shared HTML. Use `website.py links` for an optional network audit;
|
||||
distinguish missing targets from access denials and unverified fragments.
|
||||
- Check setup and support statements against the current installation/provider
|
||||
guides and released package. Preserve historical case-study checkpoints and
|
||||
versions unless a new investigation supplies replacement evidence.
|
||||
- Keep `website-pages.yml` as the sole, manual production publisher for
|
||||
Cloudflare and Pages. Default to `both`: both hosts receive the same verified
|
||||
artifact and pass the published-commit check. Use `pages` only when explicitly
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@
|
|||
An English static website with explanatory figures, worked guides and Aegis, DX-Ball, Notion, TH04 and CTF investigations.
|
||||
The public files are in `website/public/`. The site uses HTML, CSS and small
|
||||
scripts for copying code, following code comparisons and playing the dinosaur
|
||||
speed reconstruction. Python prepares the example ZIP, sitemap and sharing
|
||||
image; there is no frontend bundler or npm dependency.
|
||||
speed reconstruction. Small Python commands maintain shared HTML, prepare
|
||||
downloads and check the site. There is no frontend framework, bundler or npm
|
||||
dependency. The checked-in HTML works as ordinary static files.
|
||||
|
||||
[style-guide.md](style-guide.md) explains the writing, page structure, figures,
|
||||
visual system and review process. Read it before adding or revising a page.
|
||||
|
|
@ -21,8 +22,7 @@ From the repository root:
|
|||
python3 -m venv website/.venv
|
||||
source website/.venv/bin/activate
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
python3 scripts/prepare-website.py
|
||||
python3 -m http.server 4173 --bind 127.0.0.1 --directory website/public
|
||||
python3 scripts/website.py serve
|
||||
```
|
||||
|
||||
Open <http://127.0.0.1:4173/>. Refresh the browser after editing a file.
|
||||
|
|
@ -32,9 +32,77 @@ install them with `sudo apt-get install libcairo2 fonts-dejavu-core`. For other
|
|||
systems, see [CairoSVG's installation instructions](https://cairosvg.org/documentation/#installation).
|
||||
CI installs both packages explicitly before rendering the card.
|
||||
|
||||
After adding, moving or removing a page, or changing its indexing policy, rerun
|
||||
`python3 scripts/prepare-website.py`. It regenerates the sitemap from the current
|
||||
HTML files. No URL list needs to be maintained.
|
||||
To test the GitHub Pages prefix, use
|
||||
`python3 scripts/website.py serve --port 4174 --base-path /rea/` and open
|
||||
<http://127.0.0.1:4174/rea/>.
|
||||
|
||||
## Contribute a page or correction
|
||||
|
||||
Edit the page's HTML in `public/`. Keep the question, evidence and result easy to
|
||||
follow. The writing and design guidance in [style-guide.md](style-guide.md)
|
||||
applies to every page.
|
||||
|
||||
| Change | Edit | Then run |
|
||||
| ------------------------------ | ----------------------------------------------------------- | ----------------------------------------------- |
|
||||
| Page text, examples or figures | The page's HTML outside the marked shared regions | `python3 scripts/website.py check` |
|
||||
| Search title or description | The page's `<title>` and first description meta tag | `python3 scripts/website.py sync`, then `check` |
|
||||
| Main navigation | `NAVIGATION` in `scripts/website.py` | `sync`, then `check` |
|
||||
| Header, social links or footer | `templates/header.html` or `templates/footer.html` | `sync`, then `check` |
|
||||
| Shared link previews | `templates/sharing.html` or `public/assets/social-card.svg` | `sync`, then `check` |
|
||||
|
||||
Create a reading page with:
|
||||
|
||||
```sh
|
||||
python3 scripts/website.py new-page blog/my-article \
|
||||
--title "My article title" \
|
||||
--description "One sentence explaining the article."
|
||||
```
|
||||
|
||||
The command creates `public/blog/my-article/index.html` with the correct relative
|
||||
paths, navigation and sharing metadata. It starts as a visible draft with
|
||||
`noindex`. Write the article, add its link to the Blog index, then remove the
|
||||
Draft label and robots meta tag when it is ready. The same command accepts
|
||||
routes under `guides/` and `showcase/`. New pages still need their own content;
|
||||
the scaffold supplies the common structure.
|
||||
|
||||
The regions marked `website:sharing`, `website:header` and `website:footer` are
|
||||
generated from the templates. Edit the template rather than a generated copy.
|
||||
`sync` updates only those regions; page bodies, figures and scripts remain
|
||||
authored HTML. Commit the updated pages alongside the template. Titles and
|
||||
descriptions are written once per page; their sharing fields are derived from
|
||||
them. Routes come from file paths, including the sitemap, so there is no second
|
||||
page list to register.
|
||||
|
||||
Before opening a PR:
|
||||
|
||||
```sh
|
||||
python3 scripts/website.py check
|
||||
```
|
||||
|
||||
This checks shared HTML without rewriting it, prepares generated assets, and
|
||||
checks page links, copied-text targets, repository documentation links, SEO
|
||||
metadata and the publishing configuration. It also runs the maintenance
|
||||
regressions. A stale shared region fails with the command needed to fix it.
|
||||
PR checks and the manual publisher run this same command. The checks work
|
||||
offline after the Python dependencies are installed.
|
||||
|
||||
For an optional external-link audit, with GitHub CLI available:
|
||||
|
||||
```sh
|
||||
python3 scripts/website.py links --report /tmp/rea-website-links.json
|
||||
```
|
||||
|
||||
GitHub source links are checked through `gh`, including pinned commits and
|
||||
branch names containing slashes. Other external links use HTTP. Missing targets
|
||||
fail the command; access denials, timeouts and GitHub UI routes are reported as
|
||||
unknown and need manual verification. External fragment IDs are not checked.
|
||||
This network audit runs separately so third-party uptime does not block every PR.
|
||||
Keep its report outside public assets.
|
||||
|
||||
Links are only part of a content review. When updating setup or support claims,
|
||||
compare them with [installation.md](../docs/installation.md), the relevant
|
||||
provider guide and the released package. A case study's pinned version and date
|
||||
describe its recorded investigation; update them only when repeating that work.
|
||||
|
||||
## Pages
|
||||
|
||||
|
|
@ -67,11 +135,11 @@ navigation and asset references remain relative so both hosts and local previews
|
|||
keep working.
|
||||
|
||||
Maintain a descriptive `<title>` and a short meta description in each page's
|
||||
`<head>`. The title names the page's subject; visible headings keep the wording
|
||||
that best explains it to a reader. Open Graph and Twitter titles/descriptions
|
||||
match those fields. `og:url` matches the canonical, and sharing images use
|
||||
absolute rea.tools URLs. Search results and link previews should follow the same
|
||||
clear, concise writing standard as the page.
|
||||
`<head>`, then run `sync`. The title names the page's subject; visible headings
|
||||
keep the wording that best explains it to a reader. The script derives Open
|
||||
Graph and Twitter fields and the canonical from those inputs and the file's
|
||||
route. Search results and link previews should follow the same clear, concise
|
||||
writing standard as the page.
|
||||
|
||||
`scripts/prepare-website.py` scans `public/**/*.html` and generates
|
||||
`public/sitemap.xml` with absolute rea.tools URLs. An `index.html` maps to its
|
||||
|
|
@ -101,8 +169,7 @@ canonicals, lost fixture exclusions, stale sitemap entries and missing previews.
|
|||
Run it after preparing assets:
|
||||
|
||||
```sh
|
||||
python3 scripts/verify-website.py
|
||||
python3 scripts/test-website.py
|
||||
python3 scripts/website.py check
|
||||
```
|
||||
|
||||
### After publishing an SEO change
|
||||
|
|
@ -404,8 +471,7 @@ dependencies. The same asset preparation and verification run before each manual
|
|||
deployment. With the preview environment active, run them locally:
|
||||
|
||||
```sh
|
||||
python3 scripts/prepare-website.py
|
||||
python3 scripts/verify-website.py
|
||||
python3 scripts/website.py check
|
||||
```
|
||||
|
||||
`.github/workflows/pages.yml` is a separate, manual-only VitePress build. It
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Thoughts on reverse engineering, drawing on practical experience with REA and AI agents."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/blog/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../" aria-label="REA home"
|
||||
|
|
@ -63,8 +66,8 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/">Showcases</a>
|
||||
<a href="./" aria-current="page">Blog</a>
|
||||
<a class="nav-start" href="../get-started/">Get started</a>
|
||||
<a href="../blog/" aria-current="page">Blog</a>
|
||||
<a href="../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../guides/">Guides</a>
|
||||
<a href="../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -130,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../">Home</a> / Blog</p>
|
||||
|
|
@ -154,6 +158,7 @@
|
|||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -170,5 +175,6 @@
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,8 @@
|
|||
name="description"
|
||||
content="N0zoM1z0 reflects on manual reverse engineering, Touhou reconstruction with AI agents, and how knowledge compounds across TH08, TH095 and PC-98 TH04."
|
||||
/>
|
||||
<link
|
||||
rel="canonical"
|
||||
href="https://rea.tools/blog/touhou-reconstruction/"
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/blog/touhou-reconstruction/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
<meta
|
||||
|
|
@ -22,10 +20,7 @@
|
|||
property="og:description"
|
||||
content="N0zoM1z0 reflects on manual reverse engineering, Touhou reconstruction with AI agents, and how knowledge compounds across TH08, TH095 and PC-98 TH04."
|
||||
/>
|
||||
<meta
|
||||
property="og:url"
|
||||
content="https://rea.tools/blog/touhou-reconstruction/"
|
||||
/>
|
||||
<meta property="og:url" content="https://rea.tools/blog/touhou-reconstruction/" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://rea.tools/assets/social-card.png"
|
||||
|
|
@ -54,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -61,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top" class="blog-article">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -69,8 +66,8 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/">Showcases</a>
|
||||
<a href="../" aria-current="true">Blog</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../blog/" aria-current="page">Blog</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -136,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -1010,6 +1008,7 @@
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -1023,8 +1022,9 @@
|
|||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top"
|
||||
><span aria-hidden="true">↑</span> Top</a
|
||||
>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Read the dinosaur game’s speed rule with REA, rebuild it, then change the speed and play."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/examples/dino-lab/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -56,6 +58,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -64,9 +67,10 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/">Showcases</a>
|
||||
<a href="../../blog/">Blog</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../../guides/">Guides</a><a href="../../faq/">FAQ</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -130,6 +134,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -396,6 +401,7 @@
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -409,8 +415,9 @@
|
|||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top"
|
||||
><span aria-hidden="true">↑</span> Top</a
|
||||
>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Answers to common REA questions: installation, updates, analysis tools, coding agents, troubleshooting and sharing examples."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/faq/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../" aria-label="REA home"
|
||||
|
|
@ -64,9 +67,9 @@
|
|||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/">Showcases</a>
|
||||
<a href="../blog/">Blog</a>
|
||||
<a class="nav-start" href="../get-started/">Get started</a>
|
||||
<a href="../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../guides/">Guides</a>
|
||||
<a href="./" aria-current="page">FAQ</a>
|
||||
<a href="../faq/" aria-current="page">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -130,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../">Home</a> / FAQ</p>
|
||||
|
|
@ -495,6 +499,7 @@
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -508,8 +513,9 @@
|
|||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top"
|
||||
><span aria-hidden="true">↑</span> Top</a
|
||||
>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Try REA on a small Notes app. Set up your agent, ask where exported notes go, and check the CSV formatter with a sample title."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/first-investigation/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../" aria-label="REA home"
|
||||
|
|
@ -63,9 +66,10 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/">Showcases</a>
|
||||
<a href="../blog/">Blog</a
|
||||
><a class="nav-start" href="../get-started/">Get started</a
|
||||
><a href="../guides/">Guides</a><a href="../faq/">FAQ</a>
|
||||
<a href="../blog/">Blog</a>
|
||||
<a href="../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../guides/">Guides</a>
|
||||
<a href="../faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -129,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -433,6 +438,7 @@ JS</code></pre>
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -446,8 +452,9 @@ JS</code></pre>
|
|||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top"
|
||||
><span aria-hidden="true">↑</span> Top</a
|
||||
>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Connect REA to your coding agent or inspect a local JavaScript or Electron app from the terminal. Setup, first questions and native provider guides."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/get-started/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../" aria-label="REA home"
|
||||
|
|
@ -63,9 +66,10 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/">Showcases</a>
|
||||
<a href="../blog/">Blog</a
|
||||
><a class="nav-start" href="./" aria-current="page">Get started</a
|
||||
><a href="../guides/">Guides</a><a href="../faq/">FAQ</a>
|
||||
<a href="../blog/">Blog</a>
|
||||
<a href="../get-started/" class="nav-start" aria-current="page">Get started</a>
|
||||
<a href="../guides/">Guides</a>
|
||||
<a href="../faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -129,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../">Home</a> / Get started</p>
|
||||
|
|
@ -291,7 +296,9 @@
|
|||
<div>
|
||||
<p>
|
||||
Use an existing Ghidra 12.1.x installation and a 64-bit
|
||||
JDK 21 or newer on Linux x64 or macOS x64/arm64. Set the
|
||||
JDK 21 or newer on Linux or macOS, on x64 or arm64. Your
|
||||
Ghidra installation needs a native decompiler for your
|
||||
host architecture. Set the
|
||||
paths before running setup:
|
||||
</p>
|
||||
<details class="evidence-details">
|
||||
|
|
@ -316,8 +323,38 @@ npx rea-agents@latest setup</code></pre>
|
|||
<p>
|
||||
Replace both paths with your existing installations.
|
||||
Setup checks them and records valid settings for your
|
||||
selected client. On macOS, use a Ghidra package with the
|
||||
native decompiler matching your host architecture.
|
||||
selected client. On Linux arm64, you may need to build
|
||||
Ghidra's native decompiler separately.
|
||||
</p>
|
||||
</details>
|
||||
<details class="evidence-details">
|
||||
<summary>Connect Ghidra on Windows</summary>
|
||||
<p>
|
||||
Windows x64 has experimental, read-only support for
|
||||
native x86 and x64 PE applications and DLLs on fixed local
|
||||
NTFS drives. Use an existing Ghidra 12.1.x installation
|
||||
and a 64-bit JDK 21 or newer:
|
||||
</p>
|
||||
<div class="code-block">
|
||||
<div class="code-header">
|
||||
<span>Ghidra connection · Windows PowerShell</span>
|
||||
<button
|
||||
class="copy-button"
|
||||
type="button"
|
||||
data-copy="ghidra-windows-connect"
|
||||
aria-label="Copy Ghidra connection for Windows PowerShell"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<pre><code id="ghidra-windows-connect">$env:GHIDRA_INSTALL_DIR = "C:/tools/ghidra_12.1.4_PUBLIC"
|
||||
$env:JAVA_HOME = "C:/tools/jdk-21"
|
||||
npx -y rea-agents@latest doctor --provider ghidra --json
|
||||
npx rea-agents@latest setup</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Replace the two paths with your installations, then review
|
||||
setup's plan.
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
|
|
@ -492,6 +529,7 @@ npx rea-agents@latest setup</code></pre>
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -508,5 +546,6 @@ npx rea-agents@latest setup</code></pre>
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@
|
|||
name="description"
|
||||
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/guides/browser/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
<meta property="og:title" content="Browser and Network Analysis — REA" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Browser and Network Analysis — REA"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
|
||||
|
|
@ -29,7 +33,10 @@
|
|||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Browser and Network Analysis — REA" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Browser and Network Analysis — REA"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Observe a Notes export with REA: select a browser page, capture a click, inspect the request and follow the export script."
|
||||
|
|
@ -42,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -49,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -57,9 +66,10 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/">Showcases</a>
|
||||
<a href="../../blog/">Blog</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/" aria-current="page">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -123,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -416,6 +427,7 @@ link.click();</code></pre>
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -432,5 +444,6 @@ link.click();</code></pre>
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@
|
|||
name="description"
|
||||
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/guides/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
<meta property="og:title" content="Reverse Engineering Guides — REA" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Reverse Engineering Guides — REA"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
|
||||
|
|
@ -29,7 +33,10 @@
|
|||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Reverse Engineering Guides — REA" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Reverse Engineering Guides — REA"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Learn REA through short worked examples: a native calculation, an Electron CSV export and a browser request."
|
||||
|
|
@ -42,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../assets/styles.css" />
|
||||
|
|
@ -49,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../" aria-label="REA home"
|
||||
|
|
@ -57,9 +66,10 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../showcase/">Showcases</a>
|
||||
<a href="../blog/">Blog</a
|
||||
><a class="nav-start" href="../get-started/">Get started</a
|
||||
><a href="./" aria-current="page">Guides</a><a href="../faq/">FAQ</a>
|
||||
<a href="../blog/">Blog</a>
|
||||
<a href="../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../guides/" aria-current="page">Guides</a>
|
||||
<a href="../faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -123,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../">Home</a> / Guides</p>
|
||||
|
|
@ -199,6 +210,7 @@
|
|||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -215,5 +227,6 @@
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Trace a Notes CSV export through a renderer API, preload and IPC handler with REA. Includes downloadable example files and a verified analysis result."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/guides/javascript/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -63,9 +66,10 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/">Showcases</a>
|
||||
<a href="../../blog/">Blog</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/" aria-current="page">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -129,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -418,6 +423,7 @@
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -434,5 +440,6 @@
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@
|
|||
name="description"
|
||||
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/guides/native/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
<meta property="og:title" content="Native Binary Analysis — REA" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="Native Binary Analysis — REA"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
|
||||
|
|
@ -29,7 +33,10 @@
|
|||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="Native Binary Analysis — REA" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="Native Binary Analysis — REA"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Recover a native calculation with REA: inspect instructions, follow a caller, read constants and check reconstructed C."
|
||||
|
|
@ -42,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -49,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -57,9 +66,10 @@
|
|||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../../showcase/">Showcases</a>
|
||||
<a href="../../blog/">Blog</a
|
||||
><a class="nav-start" href="../../get-started/">Get started</a
|
||||
><a href="../">Guides</a><a href="../../faq/">FAQ</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/" aria-current="page">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -123,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -348,6 +359,7 @@ return (DxBallInt)pan;</code></pre>
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -364,5 +376,6 @@ return (DxBallInt)pan;</code></pre>
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Reverse engineer apps, binaries and browser behavior with your coding agent. Follow clear guides and try interactive examples with REA."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="assets/styles.css" />
|
||||
|
|
@ -57,6 +59,7 @@
|
|||
</head>
|
||||
<body class="home-page" id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="./" aria-label="REA home"
|
||||
|
|
@ -64,11 +67,11 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="showcase/">Showcases</a>
|
||||
<a href="blog/">Blog</a>
|
||||
<a class="nav-start" href="get-started/">Get started</a>
|
||||
<a href="guides/">Guides</a>
|
||||
<a href="faq/">FAQ</a>
|
||||
<a href="./showcase/">Showcases</a>
|
||||
<a href="./blog/">Blog</a>
|
||||
<a href="./get-started/" class="nav-start">Get started</a>
|
||||
<a href="./guides/">Guides</a>
|
||||
<a href="./faq/">FAQ</a>
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
|
|
@ -132,6 +135,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<details class="home-page-nav" data-home-nav open>
|
||||
<summary class="container">
|
||||
<span>On this page</span>
|
||||
|
|
@ -1095,6 +1099,7 @@ SPEED: 6</code></pre>
|
|||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -1111,5 +1116,6 @@ SPEED: 6</code></pre>
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Inspect the Aegis Android APK with REA, recover its six-digit login-code calculation and try a reconstruction with an adjustable clock."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/showcase/aegis/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -56,6 +58,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -63,9 +66,9 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../">Showcases</a>
|
||||
<a href="../../showcase/" aria-current="page">Showcases</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -131,6 +134,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -731,6 +735,7 @@ npx rea-agents@latest inspect-android-package \
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -744,8 +749,9 @@ npx rea-agents@latest inspect-android-package \
|
|||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top"
|
||||
><span aria-hidden="true">↑</span> Top</a
|
||||
>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/showcase/ctf/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
|
||||
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
|
||||
/>
|
||||
<meta property="og:url" content="https://rea.tools/showcase/ctf/" />
|
||||
<meta
|
||||
|
|
@ -38,7 +39,7 @@
|
|||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
|
||||
content="Use REA to solve DownUnderCTF's masked squares flag checker. Find the checking functions, extract the masks and compare a recovered flag with the original program."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -62,9 +65,9 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../">Showcases</a>
|
||||
<a href="../../showcase/" aria-current="page">Showcases</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -130,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -701,6 +705,7 @@ chmod +x ms_flag_checker
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -714,8 +719,9 @@ chmod +x ms_flag_checker
|
|||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top"
|
||||
><span aria-hidden="true">↑</span> Top</a
|
||||
>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Follow a DX-Ball reconstruction from binary analysis through REA to maintained C, original-x86 tests and matching compiled function bytes."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/showcase/dx-ball/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -62,9 +65,9 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../">Showcases</a>
|
||||
<a href="../../showcase/" aria-current="page">Showcases</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -130,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -709,6 +713,7 @@
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -725,5 +730,6 @@
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Follow REA investigations through code, figures and demos: an Android authenticator, DX-Ball, Notion, TH04 and a DownUnderCTF flag checker."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/showcase/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../" aria-label="REA home"
|
||||
|
|
@ -62,9 +65,9 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="./" aria-current="page">Showcases</a>
|
||||
<a href="../showcase/" aria-current="page">Showcases</a>
|
||||
<a href="../blog/">Blog</a>
|
||||
<a class="nav-start" href="../get-started/">Get started</a>
|
||||
<a href="../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../guides/">Guides</a>
|
||||
<a href="../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -130,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="../">Home</a> / Showcases</p>
|
||||
|
|
@ -241,6 +245,7 @@
|
|||
<a class="text-link" href="../guides/">Read the guides</a>
|
||||
</section>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -257,5 +262,6 @@
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/showcase/notion/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
|
||||
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
|
||||
/>
|
||||
<meta property="og:url" content="https://rea.tools/showcase/notion/" />
|
||||
<meta
|
||||
|
|
@ -38,7 +39,7 @@
|
|||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
|
||||
content="Follow Notion's clipboard API through Electron's preload, IPC wrappers and main process, with short code excerpts and a look at its rich clipboard format."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -62,9 +65,9 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../">Showcases</a>
|
||||
<a href="../../showcase/" aria-current="page">Showcases</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -130,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -547,6 +551,7 @@ mimeTypes:
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -563,5 +568,6 @@ mimeTypes:
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
name="description"
|
||||
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
|
||||
/>
|
||||
<!-- website:sharing:start -->
|
||||
<link rel="canonical" href="https://rea.tools/showcase/th04/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
|
|
@ -17,7 +18,7 @@
|
|||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
|
||||
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
|
||||
/>
|
||||
<meta property="og:url" content="https://rea.tools/showcase/th04/" />
|
||||
<meta
|
||||
|
|
@ -38,7 +39,7 @@
|
|||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
|
||||
content="Inspect TH04's original 16-bit DOS code with REA. Follow the assembly and recovered C++ to see how a ring of bullets is spaced and aimed at the player."
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="../../assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="../../assets/styles.css" />
|
||||
|
|
@ -55,6 +57,7 @@
|
|||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="../../" aria-label="REA home"
|
||||
|
|
@ -62,9 +65,9 @@
|
|||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
<a href="../">Showcases</a>
|
||||
<a href="../../showcase/" aria-current="page">Showcases</a>
|
||||
<a href="../../blog/">Blog</a>
|
||||
<a class="nav-start" href="../../get-started/">Get started</a>
|
||||
<a href="../../get-started/" class="nav-start">Get started</a>
|
||||
<a href="../../guides/">Guides</a>
|
||||
<a href="../../faq/">FAQ</a>
|
||||
</nav>
|
||||
|
|
@ -130,6 +133,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb">
|
||||
|
|
@ -724,6 +728,7 @@ no_aim:
|
|||
</article>
|
||||
</div>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
|
|
@ -740,5 +745,6 @@ no_aim:
|
|||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ evidence the reader can inspect, and a useful next step. A clean, refined
|
|||
appearance comes from precise wording, readable figures, consistent typography
|
||||
and deliberate spacing.
|
||||
|
||||
Use this guide for new pages and revisions. Start from an existing page and
|
||||
reuse `public/assets/styles.css` and `public/assets/main.js`. Local preview and
|
||||
Use this guide for new pages and revisions. Create the common page structure
|
||||
with `python3 scripts/website.py new-page`, then reuse the relevant components
|
||||
from an existing page, `public/assets/styles.css` and `public/assets/main.js`.
|
||||
Keep shared regions in sync with `templates/`. Contribution, preview and
|
||||
publication commands are in [README.md](README.md).
|
||||
|
||||
## What a reader should understand
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<footer class="site-footer">
|
||||
<div class="container footer-inner">
|
||||
<p>REA · Reverse Engineer Anything</p>
|
||||
<div class="footer-links">
|
||||
<a href="https://github.com/morluto/rea">Source</a
|
||||
><a href="https://github.com/morluto/rea/blob/main/LICENSE"
|
||||
>MIT license</a
|
||||
><a href="https://discord.gg/GkcryMnJDM">Join the community</a
|
||||
><a href="https://x.com/reatools">Follow us on X</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<p class="sr-only" id="copy-status" role="status" aria-live="polite"></p>
|
||||
<a class="back-to-top" href="#top" aria-label="Back to top">
|
||||
<span aria-hidden="true">↑</span> Top
|
||||
</a>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<header class="site-header">
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href="$base" aria-label="REA home"
|
||||
><span class="brand-name">REA</span
|
||||
><span class="brand-caption">Reverse Engineer Anything</span></a
|
||||
>
|
||||
<nav class="navigation" aria-label="Main navigation">
|
||||
$navigation
|
||||
</nav>
|
||||
<div class="header-social">
|
||||
<a
|
||||
class="nav-github"
|
||||
href="https://github.com/morluto/rea"
|
||||
aria-label="REA on GitHub"
|
||||
title="REA on GitHub"
|
||||
>
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 16 16"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.65 7.65 0 0 1 2-.27c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="nav-github-count" hidden aria-hidden="true"></span>
|
||||
</a>
|
||||
<a
|
||||
class="nav-discord"
|
||||
href="https://discord.gg/GkcryMnJDM"
|
||||
aria-label="Join REA on Discord"
|
||||
title="Join REA on Discord"
|
||||
>
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.317 4.3698a19.7913 19.7913 0 0 0-4.8851-1.5152.0741.0741 0 0 0-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 0 0-.0785-.037 19.7363 19.7363 0 0 0-4.8852 1.515.0699.0699 0 0 0-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 0 0 .0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 0 0 .0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 0 0-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 0 1-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 0 1 .0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 0 1 .0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 0 1-.0066.1276 12.2986 12.2986 0 0 1-1.873.8914.0766.0766 0 0 0-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 0 0 .0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 0 0 .0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 0 0-.0312-.0286ZM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189Zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
class="nav-x"
|
||||
href="https://x.com/reatools"
|
||||
aria-label="Follow REA on X"
|
||||
title="Follow REA on X"
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>$title</title>
|
||||
<meta name="description" content="$description" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<!-- website:sharing:start -->
|
||||
<!-- website:sharing:end -->
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<link rel="icon" href="${base}assets/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="${base}assets/styles.css" />
|
||||
<script src="${base}assets/main.js" defer></script>
|
||||
</head>
|
||||
<body id="top">
|
||||
<a class="skip-link" href="#main">Skip to content</a>
|
||||
<!-- website:header:start -->
|
||||
<!-- website:header:end -->
|
||||
<main id="main" class="container">
|
||||
<div class="page-heading">
|
||||
<p class="breadcrumb"><a href="$base">Home</a> / Draft</p>
|
||||
<h1>$title</h1>
|
||||
<p class="lede">$description</p>
|
||||
</div>
|
||||
<article class="article">
|
||||
<section class="article-section" id="question">
|
||||
<h2>The question</h2>
|
||||
<p>Introduce one concrete question, then show the evidence and result.</p>
|
||||
</section>
|
||||
</article>
|
||||
</main>
|
||||
<!-- website:footer:start -->
|
||||
<!-- website:footer:end -->
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<link rel="canonical" href="$canonical" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="REA" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="$title"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="$description"
|
||||
/>
|
||||
<meta property="og:url" content="$canonical" />
|
||||
<meta
|
||||
property="og:image"
|
||||
content="$origin/assets/social-card.png"
|
||||
/>
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta
|
||||
property="og:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
name="twitter:title"
|
||||
content="$title"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="$description"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image"
|
||||
content="$origin/assets/social-card.png"
|
||||
/>
|
||||
<meta
|
||||
name="twitter:image:alt"
|
||||
content="REA — Find out how software works. Reverse engineering with your coding agent."
|
||||
/>
|
||||
|
|
@ -11,7 +11,7 @@ not_found_handling = "none"
|
|||
|
||||
[build]
|
||||
cwd = ".."
|
||||
command = "python3 scripts/prepare-website.py && python3 scripts/verify-website.py"
|
||||
command = "python3 scripts/website.py check"
|
||||
watch_dir = "./public/examples/notes-electron"
|
||||
|
||||
# Public HTTPS depends on zone activation and certificate readiness.
|
||||
|
|
|
|||
Loading…
Reference in New Issue