1// Pure helpers for the ?record=ID deep link, split out of the window-bound 2// navigation code so they can be unit-tested without a browser (#422). 3// 4// router.js imports viewState.js with a ?v= suffix that Node's test loader can't 5// resolve, so the suite can't import router.js directly. Keeping these two pure 6// functions here â with no ?v= imports of their own â lets the behavioural tests 7// import them by bare relative path while the browser code imports them with the 8// usual ?v= cache-busting suffix. Browser call sites delegate here, so there is 9// one definition of the read, write, canonical-link, and share-target rules. 10 11// Read the record id from a URL query string ('?record=ID', 'record=ID', or a 12// full search with other params). Returns the id, or null when the param is 13// absent or present-but-empty â an empty id must not open a modal or pin a 14// phantom selection. Tolerates undefined/null search (URLSearchParams coerces 15// the empty string). 16export function parseRecordId(search) { 17 const params = new URLSearchParams(search || ''); 18 return params.get('record') || null; 19} 20
21// Set or clear ?record= on a URLSearchParams: a truthy id sets it, any falsy id 22// (null on deselect, undefined, '') deletes it. Returns the same params object so 23// callers can chain. This is the single source of the write-effect branch that 24// the mount-time deep-link race used to get wrong. 25export function setRecordParam(searchParams, recordId) { 26 if (recordId) { 27 searchParams.set('record', recordId); 28 } else { 29 searchParams.delete('record'); 30 } 31 return searchParams; 32} 33 34// Build the one shareable record URL used by every archive surface. Desktop 35// windows intentionally resolve to the standard archive record URL so the 36// optional shell never creates a second incompatible citation/share format. 37export function canonicalRecordUrl(locationHref, recordId) { 38 if (typeof locationHref !== 'string' || locationHref === '') { 39 throw new TypeError('canonicalRecordUrl requires a location URL'); 40 } 41 if (typeof recordId !== 'string' || recordId === '') { 42 throw new TypeError('canonicalRecordUrl requires a record id'); 43 } 44 45 const url = new URL(locationHref); 46 url.search = ''; 47 url.hash = ''; 48 setRecordParam(url.searchParams, recordId); 49 return url.toString(); 50} 51 52// Article-like records use the generated archive metadata route. Social posts 53// use their original public URL so the platform's own record-specific preview 54// is available without generating tens of thousands of extra FTP files. 55export function shareRecordUrl(locationHref, record) { 56 if (!record || typeof record.id !== 'string' || record.id === '') { 57 throw new TypeError('shareRecordUrl requires a record with an id'); 58 } 59 60 if (record.type === 'social' && typeof record.url === 'string') { 61 try { 62 const sourceUrl = new URL(record.url); 63 if (sourceUrl.protocol === 'http:' || sourceUrl.protocol === 'https:') { 64 return sourceUrl.toString(); 65 } 66 } catch { 67 // Fall through to the safe archive URL. 68 } 69 } 70 71 return canonicalRecordUrl(locationHref, record.id); 72} 73 74export default { canonicalRecordUrl, parseRecordId, setRecordParam, shareRecordUrl };
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.