|
# retoor <retoor@molodetz.nl>
|
|
|
|
import re
|
|
|
|
from devplacepy.rendering import _render_content
|
|
from devplacepy.seo import truncate
|
|
from devplacepy.utils import strip_html
|
|
|
|
TITLE_MAX = 60
|
|
DESCRIPTION_MAX = 160
|
|
KEYWORDS_MAX = 8
|
|
|
|
_STOPWORDS = {
|
|
"the", "a", "an", "and", "or", "but", "of", "to", "in", "on", "for", "with",
|
|
"at", "by", "from", "as", "is", "are", "was", "were", "be", "been", "this",
|
|
"that", "these", "those", "it", "its", "into", "about", "over", "your",
|
|
"you", "we", "our", "i", "my", "me", "they", "them", "their", "he", "she",
|
|
"his", "her", "not", "no", "yes", "can", "will", "would", "should", "could",
|
|
"has", "have", "had", "do", "does", "did", "so", "if", "than", "then",
|
|
"there", "here", "what", "which", "who", "when", "where", "why", "how",
|
|
"all", "any", "some", "more", "most", "other", "such", "only", "own",
|
|
"just", "also", "very", "too", "out", "up", "down", "off", "now", "new",
|
|
}
|
|
|
|
_WORD_RE = re.compile(r"[a-z0-9][a-z0-9\+\#\.\-]{1,}")
|
|
|
|
|
|
def plain_text_from_markdown(text) -> str:
|
|
if not text:
|
|
return ""
|
|
rendered = _render_content(str(text))
|
|
return strip_html(rendered)
|
|
|
|
|
|
def _clamp_title(title: str) -> str:
|
|
title = " ".join((title or "").replace("\n", " ").split())
|
|
if len(title) <= TITLE_MAX:
|
|
return title
|
|
clipped = title[:TITLE_MAX].rsplit(" ", 1)[0]
|
|
return clipped or title[:TITLE_MAX]
|
|
|
|
|
|
def _clamp_description(description: str) -> str:
|
|
return truncate(" ".join((description or "").split()), DESCRIPTION_MAX)
|
|
|
|
|
|
def derive_keywords(title: str, body: str, limit: int = KEYWORDS_MAX) -> str:
|
|
text = f"{title or ''} {body or ''}".lower()
|
|
seen: list[str] = []
|
|
for match in _WORD_RE.finditer(text):
|
|
word = match.group(0).strip(".-")
|
|
if len(word) < 3 or word in _STOPWORDS or word in seen:
|
|
continue
|
|
seen.append(word)
|
|
if len(seen) >= limit:
|
|
break
|
|
return ", ".join(seen)
|
|
|
|
|
|
def plain_seo_defaults(title, body) -> dict:
|
|
clean_title = _clamp_title(plain_text_from_markdown(title) or (str(title or "")))
|
|
body_text = plain_text_from_markdown(body)
|
|
description = _clamp_description(body_text or clean_title)
|
|
keywords = derive_keywords(clean_title, body_text)
|
|
return {
|
|
"title": clean_title or "DevPlace",
|
|
"description": description or clean_title or "DevPlace",
|
|
"keywords": keywords,
|
|
}
|
|
|
|
|
|
def clamp_generated(title: str, description: str, keywords: str) -> dict:
|
|
clean_title = _clamp_title(plain_text_from_markdown(title) or title)
|
|
clean_description = _clamp_description(plain_text_from_markdown(description) or description)
|
|
terms = [
|
|
term.strip()
|
|
for term in re.split(r"[,\n;]+", keywords or "")
|
|
if term.strip()
|
|
]
|
|
cleaned: list[str] = []
|
|
for term in terms:
|
|
lowered = " ".join(term.lower().split())
|
|
if lowered and lowered not in cleaned:
|
|
cleaned.append(lowered)
|
|
if len(cleaned) >= KEYWORDS_MAX:
|
|
break
|
|
return {
|
|
"title": clean_title,
|
|
"description": clean_description,
|
|
"keywords": ", ".join(cleaned),
|
|
}
|