"""Work and document keys: a Scholar-style citation key plus a short hash of the work's identity. Non-normative; see spec/02-model.md §2.4. A work key looks like `vaswani2017attention-k3f9`. The first part is what Google Scholar would call the work; the characters after the hyphen come from a hash of the work's identity, so two works Scholar would name alike never share a key. A document key is the work key with a suffix for anything other than the version of record. """ import hashlib import re import unicodedata from .text import ALPHABET SCHEME = "scienting-key/1" SUFFIX_LENGTH = 4 MAX_SUFFIX = 51 IDENTIFIER_ORDER = ("doi", "arxiv", "isbn", "pmid", "pmcid", "openalex") WORK_KEY_RE = re.compile(r"^[a-z][a-z0-9]*-[0-9a-hjkmnp-tv-z]{4,51}$") DOCUMENT_KEY_RE = re.compile(r"^[a-z][a-z0-9]*-[0-9a-hjkmnp-tv-z]{4,51}(\.[a-z0-9][a-z0-9-]*)*$") STOP_WORDS = frozenset("a an the of on in for to and or at by with from as into onto about toward towards via " "is are be its it this that these those".split()) NAME_SUFFIXES = frozenset(["jr", "sr", "ii", "iii", "iv", "v", "phd", "md"]) _YEAR = re.compile(r"^\d{4}$") _DOI_PREFIX = re.compile(r"^(?:doi:|https?://(?:dx\.|www\.)?doi\.org/)+", re.IGNORECASE) def fold(text): """Lower-case ASCII with accents removed: `Müller` gives `muller`.""" raw = text if isinstance(text, str) else "" return unicodedata.normalize("NFKD", raw).encode("ascii", "ignore").decode().lower() def title_words(title): """A title's words, folded, with apostrophes removed and other punctuation splitting.""" t = re.sub(r"['’`]", "", fold(title)) return re.findall(r"[a-z0-9]+", t) def family_name(name): """A best guess at the family name in a display name: the part before a comma, else the last word that is not a suffix such as `Jr.` or `III`.""" name = (name or "").strip() if "," in name: head, _, tail = name.partition(",") if tail.strip().rstrip(".").lower() not in NAME_SUFFIXES or " " not in head.strip(): return head.strip() name = head parts = name.split() while len(parts) > 1 and parts[-1].rstrip(".").lower() in NAME_SUFFIXES: parts.pop() return parts[-1] if parts else "" def first_family(meta): """The first author's family name from a CSL-JSON item, as the key spells it, or `anon`.""" authors = meta.get("author") or [] if not authors: return "anon" a = authors[0] name = a.get("family") or family_name(a.get("literal") or a.get("name") or "") fam = re.sub(r"[^a-z]", "", fold(name)) return fam or "anon" def year_of(meta): """The year of a CSL-JSON item's `issued` date, when it is a four-digit year, else the empty string.""" parts = (meta.get("issued") or {}).get("date-parts") or [] y = parts[0][0] if parts and parts[0] else None s = str(y) if isinstance(y, int) and not isinstance(y, bool) else (y if isinstance(y, str) else "") return s if _YEAR.match(s) else "" def scholar_part(meta): """Family name, year and the first title word that is not a stop word: `vaswani2017attention`.""" word = next((w for w in title_words(meta.get("title")) if w not in STOP_WORDS), "") return first_family(meta) + year_of(meta) + word def norm_doi(value): """A DOI in bare form with ASCII letters lower-cased (DOI Handbook 2.4).""" s = _DOI_PREFIX.sub("", value.strip()).strip() return s.translate(str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")) def identity(same_as, meta): """What the key's hash is computed over: the first identifier of IDENTIFIER_ORDER among `same_as`, else the folded metadata `meta: