Baseline - Eddys Nova Estrutura

This commit is contained in:
Deyson Thomé
2026-09-29 14:03:31 -03:00
commit f08d23dc3c
436 changed files with 21279 additions and 0 deletions
+458
View File
@@ -0,0 +1,458 @@
#!/usr/bin/env python3
"""
build_site.py - lê site-manifest.json e escreve os blocos repetidos das páginas.
Comandos (rode na pasta do site, ao lado do site-manifest.json):
scan compara pastas x manifesto: acrescenta páginas novas e preenche status que faltam
init coloca as marcas <!-- NOME:START --> / <!-- NOME:END --> nas páginas que ainda não têm
apply reescreve o conteúdo entre as marcas (use --dry-run para só ver o que mudaria)
check confere manifesto x disco, marcas, blocos desatualizados, links e endereços antigos
O script só escreve DENTRO das marcas. Nada fora delas é tocado.
"""
import argparse, fnmatch, json, os, re, sys
from pathlib import Path
BLOCKS = ["TRACKING", "HEADER", "NAV", "BREADCRUMB", "FOOTER", "CONTACT"]
OPTIONAL = {"CONTACT"} # só existe em páginas com o cartão "Contact Us" na sidebar
IGNORE_DIRS = {".git", "img", "images", "css", "js", "node_modules", "modelo"}
IGNORE_FILES = re.compile(r"^(google[0-9a-f]+|pinterest-[0-9a-z]+)\.html$")
HOME = "index.html"
# ---------------------------------------------------------------- manifesto
def load(root):
path = root / "site-manifest.json"
if not path.exists():
sys.exit(f"Não achei {path}")
m = json.loads(path.read_text(encoding="utf-8"))
m["_by"] = {p["file"]: p for p in m["pages"]}
return m
def save(root, m):
data = {k: v for k, v in m.items() if not k.startswith("_")}
head = json.dumps({k: v for k, v in data.items() if k != "pages"}, ensure_ascii=False, indent=2)
head = head.rstrip("}").rstrip().rstrip(",")
lines = ",\n".join(" " + json.dumps(p, ensure_ascii=False) for p in data["pages"])
(root / "site-manifest.json").write_text(head + ',\n "pages": [\n' + lines + "\n ]\n}\n", encoding="utf-8")
def url(m, f):
if m["options"].get("strip_index"):
if f == HOME:
return "/"
if f.endswith("/index.html"):
return "/" + f[: -len("index.html")]
return "/" + f
def pub(p):
return p.get("status") == "publicada"
def chain(m, f):
"""arquivo -> pai -> ... -> home (protegido contra ciclo)"""
out, seen = [], set()
while f and f not in seen and f in m["_by"]:
out.append(f)
seen.add(f)
f = m["_by"][f].get("parent")
return out
def esc(t):
return t.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
# ---------------------------------------------------------------- geradores
def gen_tracking(m, cur):
t = m["site"]["tracking"]
return f"""<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){{w[l]=w[l]||[];w[l].push({{'gtm.start':
new Date().getTime(),event:'gtm.js'}});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
}})(window,document,'script','dataLayer','{t['gtm']}');</script>
<!-- End Google Tag Manager -->
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {{
var u="{t['matomo_url']}";
_paq.push(['setTrackerUrl', u+'matomo.php']);
_paq.push(['setSiteId', '{t['matomo_site_id']}']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.async=true; g.src=u+'matomo.js'; s.parentNode.insertBefore(g,s);
}})();
</script>
<!-- End Matomo Code -->"""
def gen_header(m, cur):
s = m["site"]
return f"""<div class="main-header">
<div class="container header-container">
<div class="logo"><a href="{url(m, HOME)}"><img alt="{esc(s['name'])}" src="/img/logo.png"/></a></div>
<div class="info-contact">
<ul class="info-contact-list">
<li><i class="fa fa-phone"></i><p>{esc(s['phone'])}</p><p class="vice">Call Us Free</p></li>
<li><i class="fa fa-clock-o"></i><p>{esc(s['hours'])}</p><p class="vice">{esc(s['closed_note'])}</p></li>
</ul>
</div>
</div>
</div>"""
def gen_nav(m, cur):
pages = m["pages"]
ch = set(chain(m, cur))
li = []
for top in (p for p in pages if p.get("menu") == "top" and pub(p)):
active = top["file"] in ch and (top["file"] != HOME or cur == HOME)
subs = [c for c in pages if c.get("menu") == "sub" and c.get("parent") == top["file"] and pub(c)]
if not subs:
cls = ' class="active"' if active else ""
li.append(f'<li{cls}><a href="{url(m, top["file"])}">{esc(top["label"])}</a></li>')
continue
items = []
for c in subs:
cls = ' class="active"' if c["file"] in ch else ""
items.append(f' <li{cls}><a href="{url(m, c["file"])}">{esc(c["label"])}</a></li>')
if top.get("others"):
items.append(f' <li><a href="{url(m, top["file"])}">Others</a></li>')
cls = "menu-item-has-children" + (" active" if active else "")
li.append(
f'<li class="{cls}">\n'
f' <a href="{url(m, top["file"])}" aria-haspopup="true">{esc(top["label"])} <span class="fa fa-angle-down"></span></a>\n'
f' <ul class="sub-menu">\n' + "\n".join(items) + "\n </ul>\n</li>"
)
body = "\n".join(" " + l.replace("\n", "\n ") for l in li)
return f"""<div class="navBar6">
<div class="container nav-container">
<nav class="navigation" id="main-nav" aria-label="Main Navigation">
<ul class="nav text-uppercase">
{body}
</ul>
</nav>
<button aria-label="Toggle Navigation" class="mobile-nav-toggle" id="mobile-toggle"><span class="hamburger"></span></button>
</div>
</div>"""
def gen_breadcrumb(m, cur):
trail = list(reversed(chain(m, cur)))
items = []
for i, f in enumerate(trail):
p = m["_by"][f]
label = esc(p.get("crumb") or p["label"])
if i == len(trail) - 1 or not pub(p):
items.append(f" <li>{label}</li>")
else:
items.append(f' <li><a href="{url(m, f)}">{label}</a></li>')
return ('<div class="breadcrumb-container">\n <span>You are here:</span>\n <ul class="breadcrumb">\n'
+ "\n".join(items) + "\n </ul>\n</div>")
def gen_footer(m, cur):
s = m["site"]
social = "\n".join(
f' <li><a href="{x["url"]}" target="_blank" rel="noopener"><i class="fa {x["icon"]}"></i></a></li>'
for x in s.get("social", []))
fm = "\n".join(
f' <li><a href="{url(m, p["file"])}">{esc(p.get("footer_label") or p["label"])}</a></li>'
for p in m["pages"] if p.get("footer") and pub(p))
return f"""<footer class="footer bg-dark" id="footer">
<div class="foot-widget">
<div class="container">
<div class="footer-grid">
<div class="widget widget_about">
<div class="logo"><a href="{url(m, HOME)}"><img alt="{esc(s['name'])}" src="/img/logo-w.png"/></a></div>
<p>{esc(s['about_short'])}</p>
<ul class="social-list">
{social}
</ul>
</div>
<div class="widget widget_contact">
<h3 class="widget-title">Get in touch</h3>
<ul>
<li><i class="fa fa-mobile"></i>{esc(s['phone'])}</li>
<li><i class="fa fa-map-marker"></i>{esc(s['address'])}</li>
<li><i class="fa fa-paper-plane"></i>{esc(s['email'])}</li>
<li><i class="fa fa-clock-o"></i>{esc(s['hours'])}</li>
</ul>
</div>
<div class="widget widget_subscribe">
<h3 class="widget-title">Mobile Contact</h3>
<p>Quick options to call or email directly from your phone:</p>
<div class="mobile-actions">
<a class="btn btn-transparent radius" href="tel:{s['phone_tel']}">Click to Call</a>
<a class="btn btn-transparent radius" href="mailto:{s['email']}">Click to Email</a>
</div>
</div>
</div>
</div>
</div>
<div class="foot-credit">
<div class="container foot-credit-flex">
<p>{s['copyright']}</p>
<ul class="foot-menu">
{fm}
</ul>
</div>
</div>
</footer>"""
def gen_contact(m, cur):
s = m["site"]
return f"""<aside class="widget widget_contact">
<h3 class="widget-title">Contact Us</h3>
<ul>
<li><i class="fa fa-mobile"></i>{esc(s['phone'])}</li>
<li><i class="fa fa-map-marker"></i>{esc(s['address'])}</li>
<li><i class="fa fa-paper-plane"></i>{esc(s['email'])}</li>
<li><i class="fa fa-clock-o"></i>{esc(s['hours'])}</li>
</ul>
</aside>"""
GEN = {"TRACKING": gen_tracking, "HEADER": gen_header, "NAV": gen_nav,
"BREADCRUMB": gen_breadcrumb, "FOOTER": gen_footer, "CONTACT": gen_contact}
# ---------------------------------------------------------------- marcas
def marker_re(name):
return re.compile(r"([ \t]*)<!-- %s:START -->.*?<!-- %s:END -->" % (name, name), re.S)
def render(text, m, cur):
"""devolve (novo_texto, faltando) com os blocos regenerados"""
missing = []
for name in BLOCKS:
if name == "BREADCRUMB" and (cur == HOME or m["_by"][cur].get("no_breadcrumb")):
continue
rx = marker_re(name)
mt = rx.search(text)
if not mt:
if name not in OPTIONAL:
missing.append(name)
continue
ind = mt.group(1)
body = GEN[name](m, cur)
block = ind + f"<!-- {name}:START -->\n" + "\n".join(ind + l if l else l for l in body.split("\n")) \
+ f"\n{ind}<!-- {name}:END -->"
text = text[: mt.start()] + block + text[mt.end():]
return text, missing
def balanced_div(text, start):
depth = 0
for mt in re.finditer(r"<div\b|</div>", text[start:]):
depth += 1 if mt.group(0).startswith("<div") else -1
if depth == 0:
return start + mt.end()
return None
def find_span(text, name):
if name == "TRACKING":
a = text.find("<!-- Google Tag Manager -->")
b = text.find("<!-- End Matomo Code -->")
return (a, b + len("<!-- End Matomo Code -->")) if a >= 0 and b > a else None
if name == "CONTACT":
a = text.find('<aside class="widget widget_contact">')
b = text.find("</aside>", a)
return (a, b + len("</aside>")) if a >= 0 and b > a else None
if name == "FOOTER":
a = text.find("<footer")
b = text.find("</footer>", a)
return (a, b + len("</footer>")) if a >= 0 and b > a else None
cls = {"HEADER": "main-header", "NAV": "navBar6", "BREADCRUMB": "breadcrumb-container"}[name]
mt = re.search(r'<div class="%s"' % cls, text)
if not mt:
return None
end = balanced_div(text, mt.start())
return (mt.start(), end) if end else None
def add_markers(text):
added = []
for name in BLOCKS:
if f"<!-- {name}:START -->" in text:
continue
span = find_span(text, name)
if not span:
continue
a, b = span
line_start = text.rfind("\n", 0, a) + 1
ind = text[line_start:a] if not text[line_start:a].strip() else ""
text = (text[:line_start] + f"{ind}<!-- {name}:START -->\n" + text[line_start:b]
+ f"\n{ind}<!-- {name}:END -->" + text[b:])
added.append(name)
return text, added
# ---------------------------------------------------------------- comandos
def is_ignored(m, f):
"""padrões do manifesto (options.ignore), estilo curinga: 'files/*', 'resources/old-index.html'"""
return any(fnmatch.fnmatch(f, pat) for pat in m["options"].get("ignore", []))
def disk_pages(root, m=None):
for dp, dn, fn in os.walk(root):
dn[:] = [d for d in dn if d not in IGNORE_DIRS]
for f in fn:
if f.endswith(".html") and not IGNORE_FILES.match(f):
rel = str(Path(dp, f).relative_to(root)).replace(os.sep, "/")
yield rel
def label_from_h1(root, f):
try:
t = (root / f).read_text(encoding="utf-8", errors="ignore")
except OSError:
return None
mt = re.search(r"<h1[^>]*>(.*?)</h1>", t, re.S | re.I)
if not mt:
return None
txt = re.sub(r"<[^>]+>", "", mt.group(1))
txt = re.sub(r"\s+", " ", txt).strip()
return txt.replace("&amp;", "&") or None
def has_content(root, f):
p = root / f
return p.is_file() and p.stat().st_size > 0
def cmd_scan(root, m):
changed = False
for p in m["pages"]:
if "status" not in p:
p["status"] = "publicada" if has_content(root, p["file"]) else "planejada"
print(f"status definido pelo disco: {p['file']} -> {p['status']}")
changed = True
known = set(m["_by"])
ignored = []
for f in sorted(disk_pages(root)):
if f in known:
continue
if is_ignored(m, f):
ignored.append(f)
continue
folder = Path(f).parent.as_posix()
parent = (folder + "/index.html") if folder != "." and f.endswith("/index.html") is False else None
if f.endswith("/index.html"):
up = Path(folder).parent.as_posix()
parent = (up + "/index.html") if up != "." else HOME
if parent and parent not in known and parent != HOME:
parent = HOME
label = label_from_h1(root, f) or Path(f).stem.replace("-", " ").title()
page = {"file": f, "label": label, "parent": parent or HOME,
"status": "publicada" if has_content(root, f) else "planejada"}
m["pages"].append(page)
print(f"NOVA página no disco, acrescentada ao manifesto: {f} ({page['status']}). Ajuste label/parent/menu.")
changed = True
for f in ignored:
print(f"ignorada (options.ignore): {f}")
for p in m["pages"]:
if not (root / p["file"]).exists():
print(f"AVISO: no manifesto mas não existe no disco: {p['file']} ({p.get('status')})")
if changed:
save(root, m)
else:
print("Nada a acrescentar.")
def cmd_init(root, m):
for p in m["pages"]:
if not has_content(root, p["file"]):
continue
path = root / p["file"]
text = path.read_text(encoding="utf-8")
new, added = add_markers(text)
if added:
path.write_text(new, encoding="utf-8")
print(f"{p['file']}: marcas em {', '.join(added)}")
print("Pronto. Agora rode: apply --dry-run")
def cmd_apply(root, m, dry):
n = 0
for p in m["pages"]:
if not pub(p):
continue
if not has_content(root, p["file"]):
print(f"ERRO: {p['file']} está 'publicada' no manifesto mas o arquivo falta ou está vazio")
continue
path = root / p["file"]
text = path.read_text(encoding="utf-8")
new, missing = render(text, m, p["file"])
if missing:
print(f"{p['file']}: sem marcas em {', '.join(missing)} (rode init)")
if new != text:
n += 1
print(("mudaria: " if dry else "atualizada: ") + p["file"])
if not dry:
path.write_text(new, encoding="utf-8")
print(f"{n} página(s) {'mudariam' if dry else 'atualizadas'}.")
def cmd_check(root, m):
errors = warns = 0
legacy = m["site"].get("old_strings", [])
for p in m["pages"]:
f = p["file"]
if not pub(p):
continue
if not has_content(root, f):
print(f"ERRO {f}: 'publicada' mas o arquivo falta ou está vazio"); errors += 1
continue
text = (root / f).read_text(encoding="utf-8")
new, missing = render(text, m, f)
if missing:
print(f"AVISO {f}: sem marcas em {', '.join(missing)}"); warns += 1
elif new != text:
print(f"AVISO {f}: blocos desatualizados (rode apply)"); warns += 1
for s in legacy:
if s in text:
print(f"ERRO {f}: contém '{s}'"); errors += 1
visible = re.sub(r"<!--.*?-->", "", text, flags=re.S) # ignora trechos comentados
for mt in re.finditer(r'(?:href|src)="([^"#?]+)', visible):
u = mt.group(1)
if re.match(r"(https?:|mailto:|tel:|//|data:|javascript:)", u):
continue
rel = u.lstrip("/") if u.startswith("/") else os.path.normpath(os.path.join(os.path.dirname(f), u))
cand = [rel, rel.rstrip("/") + "/index.html"] if not rel.endswith(".html") else [rel]
if not any((root / c).is_file() and (root / c).stat().st_size > 0 for c in cand):
if rel.startswith(("img/", "css/", "js/", "images/")) or "." in os.path.basename(rel) and not rel.endswith(".html"):
if (root / rel).is_file():
continue
print(f"ERRO {f}: link quebrado ou para página vazia -> {u}"); errors += 1
print(f"\n{errors} erro(s), {warns} aviso(s).")
return 1 if errors else 0
def main():
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("cmd", choices=["scan", "init", "apply", "check"])
ap.add_argument("--root", default=".", help="pasta do site (padrão: pasta atual)")
ap.add_argument("--dry-run", action="store_true")
a = ap.parse_args()
root = Path(a.root).resolve()
m = load(root)
if a.cmd == "scan":
cmd_scan(root, m)
elif a.cmd == "init":
cmd_init(root, m)
elif a.cmd == "apply":
cmd_apply(root, m, a.dry_run)
else:
sys.exit(cmd_check(root, m))
if __name__ == "__main__":
main()