Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
// Package identifiers validates the scholarly identifiers a post or an
|
||||
// account can carry: a DOI names the work, an ORCID names an author. It
|
||||
// is a leaf so the configuration, the domain object, the payload
|
||||
// builders and the admin share one rule without importing one another.
|
||||
//
|
||||
// The rules are syntactic on purpose. A DOI resolves through doi.org and
|
||||
// an ORCID through orcid.org, and both publish registries; asking those
|
||||
// services here would tie a save to the network, leak who is writing to
|
||||
// a third party, and make the offline binary wait on someone else's
|
||||
// uptime. Syntax plus the ORCID check digit catches every realistic
|
||||
// typo; resolution stays the consumer's job.
|
||||
package identifiers
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var doiRe = regexp.MustCompile(`\A10\.[0-9]{4,9}/\S+\z`)
|
||||
|
||||
// orcidRe is the display form: four groups of four digits, the last
|
||||
// character a digit or the uppercase X that stands for a checksum of 10.
|
||||
var orcidRe = regexp.MustCompile(`\A[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[0-9X]\z`)
|
||||
|
||||
// NormalizeDOI accepts the bare form, a doi: scheme and a doi.org URL,
|
||||
// and returns the bare identifier; an empty result means the input
|
||||
// carries no DOI at all.
|
||||
func NormalizeDOI(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
for _, prefix := range []string{
|
||||
"https://doi.org/", "http://doi.org/", "doi.org/", "doi:",
|
||||
} {
|
||||
if len(text) > len(prefix) && strings.EqualFold(text[:len(prefix)], prefix) {
|
||||
text = strings.TrimSpace(text[len(prefix):])
|
||||
break
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// ValidDOI reports whether text is a syntactically valid bare DOI: the
|
||||
// 10. prefix, a registrant needle of four to nine digits, a slash, and
|
||||
// a non-empty suffix without spaces.
|
||||
func ValidDOI(text string) bool {
|
||||
return doiRe.MatchString(NormalizeDOI(text))
|
||||
}
|
||||
|
||||
// NormalizeORCID trims and lower-cases to upper; it fixes no body.
|
||||
func NormalizeORCID(text string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(text))
|
||||
}
|
||||
|
||||
// ValidORCID reports whether text is an ORCID iD in display form with a
|
||||
// correct ISO 7064 (MOD 11-2) check digit.
|
||||
func ValidORCID(text string) bool {
|
||||
id := NormalizeORCID(text)
|
||||
if !orcidRe.MatchString(id) {
|
||||
return false
|
||||
}
|
||||
digits := strings.ReplaceAll(id, "-", "")
|
||||
total := 0
|
||||
for _, r := range digits[:15] {
|
||||
total = (total + int(r-'0')) * 2
|
||||
}
|
||||
remainder := (12 - total%11) % 11
|
||||
want := byte('0' + remainder)
|
||||
if remainder == 10 {
|
||||
want = 'X'
|
||||
}
|
||||
return digits[15] == want
|
||||
}
|
||||
|
||||
// DOIURL names the resolver for a bare or already-prefixed DOI; an
|
||||
// invalid input yields "".
|
||||
func DOIURL(text string) string {
|
||||
doi := NormalizeDOI(text)
|
||||
if !doiRe.MatchString(doi) {
|
||||
return ""
|
||||
}
|
||||
return "https://doi.org/" + doi
|
||||
}
|
||||
|
||||
// ORCIDURL names the public record for a valid iD; an invalid input
|
||||
// yields "".
|
||||
func ORCIDURL(text string) string {
|
||||
id := NormalizeORCID(text)
|
||||
if !ValidORCID(id) {
|
||||
return ""
|
||||
}
|
||||
return "https://orcid.org/" + id
|
||||
}
|
||||
Reference in New Issue
Block a user