A shared link reads a list separated by , or |, so partner links already published keep working
Links on partner pages write band=yes|partial, and the reader split on ',' only, so such a link read back as one value and matched no row. Reading now accepts either separator; writing stays ','. A literal '|' in a value is percent-encoded before any join, so every separator left in a URL is a real one.
This commit is contained in:
parent
b6e3e005a3
commit
e79d6dc8f4
2 changed files with 29 additions and 3 deletions
|
|
@ -192,6 +192,14 @@ if (debt.length) {
|
|||
eq('alias reads an already-shared link', parseQuery('?access=low&app=sms', ALIAS), { filters: { access_level: 'low', messaging: 'sms' }, search: '' })
|
||||
eq('an undeclared bus key keeps its own name', serializeQuery({ country: 'BT' }, '', '', { facetKeys: ALIAS }), '?country=BT')
|
||||
eq('a host param survives under an alias map', serializeQuery({ access_level: 'low' }, '', '?utm=x', { facetKeys: ALIAS }), '?utm=x&access=low')
|
||||
// ★ BOTH SEPARATORS ON READ, ONE ON WRITE. go-int's board has always written band=yes|partial and
|
||||
// those links are on partner pages; splitting only on ',' read that back as the single value
|
||||
// "yes|partial" and matched no row, so adopting this composable would have broken every multi-value
|
||||
// link they had already shared. Reserving `|` on write is what makes accepting it on read free.
|
||||
eq('an already-shared | link restores', parseQuery('?band=yes|partial', ['band']).filters, { band: ['yes', 'partial'] })
|
||||
eq('mixed separators in one link', parseQuery('?band=yes|partial&country=IND,CHN', ['band', 'country']).filters, { band: ['yes', 'partial'], country: ['IND', 'CHN'] })
|
||||
eq('we still write one canonical separator', serializeQuery({ band: ['yes', 'partial'] }, '', '', { facetKeys: ['band'] }), '?band=yes,partial')
|
||||
eq('a literal | inside a value is escaped, not split', parseQuery(serializeQuery({ x: 'a|b' }, '', '', { facetKeys: ['x'] }), ['x']).filters, { x: 'a|b' })
|
||||
if (!fails.some((f) => f.startsWith('SHARE-VIEW'))) notes.push('share link: filters AND the typed search round-trip in BOTH hash and query mode; old links, legacy JSON and a host page\'s own query all survive')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,25 @@ const slugify = (s) => String(s ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
|||
// values of ONE key (OR-within-key), and is reserved for the same reason `;` `:` `*` are: a delimiter
|
||||
// that can also occur inside a value is not a delimiter. `&` joins params, so it is reserved too —
|
||||
// without that, a search for "rice & beans" would truncate the link at the ampersand.
|
||||
const escapeValue = (s) => String(s).replace(/[%;:*+,&]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()).replace(/ /g, '+')
|
||||
// `|` joins as well as `,` — see LIST_SEP below — so it is reserved too. A delimiter that can also occur
|
||||
// inside a value is not a delimiter, and that rule has already cost this tree once: the language-code link
|
||||
// joined on `,` until 120 of 864 language labels turned out to contain one.
|
||||
const escapeValue = (s) => String(s).replace(/[%;:*+,&|]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase()).replace(/ /g, '+')
|
||||
|
||||
/**
|
||||
* ★ READ BOTH `,` AND `|`; WRITE `,`. go-int's board has always written `band=yes|partial` and
|
||||
* `country=IND|CHN`, and those links are on partner pages now. Splitting only on `,` read that back as the
|
||||
* single value "yes|partial", which matches no row — so adopting this composable would have broken every
|
||||
* multi-value link they had already shared. Accepting BOTH separators on read costs nothing: escapeValue
|
||||
* percent-encodes a literal `,` or `|` inside a value before any join, so every separator still present in
|
||||
* a URL is genuinely a separator.
|
||||
* Writing stays `,` so there is ONE canonical form going forward rather than two dialects to reason about.
|
||||
* The one case this does not cover, stated rather than papered over: an OLD link whose VALUE contained a
|
||||
* literal `|` written before `|` was reserved would now be split. No facet vocabulary in the tree contains
|
||||
* one (checked religion.js and the region/language codes), and go-int confirms `|` cannot occur in a facet
|
||||
* code, so the exposure is a value shape that does not exist rather than one we are choosing to risk.
|
||||
*/
|
||||
const LIST_SEP = /[,|]/
|
||||
const unescapeValue = (s) => { try { return decodeURIComponent(String(s).replace(/\+/g, '%20')) } catch (_) { return String(s).replace(/\+/g, ' ') } }
|
||||
|
||||
/** Serialize a view to a hash string. Exported so a gate can drive it without a browser. */
|
||||
|
|
@ -88,7 +106,7 @@ export function parseView(hash, { hashKey = 'f', searchKey = 'q' } = {}) {
|
|||
// Split BEFORE unescaping: a literal comma inside a value is percent-encoded by escapeValue, so
|
||||
// every `,` still here is a separator. Unescaping first would resurrect content commas and split
|
||||
// the value in half.
|
||||
const parts = body.split(',').map(unescapeValue).filter(Boolean)
|
||||
const parts = body.split(LIST_SEP).map(unescapeValue).filter(Boolean)
|
||||
if (key && parts.length) out.filters[key] = parts.length > 1 ? parts : parts[0]
|
||||
}
|
||||
return out
|
||||
|
|
@ -153,7 +171,7 @@ export function parseQuery(search, facetKeys = [], { searchKey = 'q' } = {}) {
|
|||
if (k === searchKey) { out.search = unescapeValue(body); continue }
|
||||
const busKey = keyOf(k)
|
||||
if (!busKey) continue
|
||||
const parts = body.split(',').map(unescapeValue).filter(Boolean)
|
||||
const parts = body.split(LIST_SEP).map(unescapeValue).filter(Boolean)
|
||||
if (parts.length) out.filters[busKey] = parts.length > 1 ? parts : parts[0]
|
||||
}
|
||||
return out
|
||||
|
|
|
|||
Loading…
Reference in a new issue