Created
May 31, 2020 13:58
-
-
Save rumkin/ae8b2ec9c12de4fa4ceec89ff8b54449 to your computer and use it in GitHub Desktop.
Parse routes for CSS props
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import escapeRegex from 'escape-regexp' | |
// Tokens | |
const T_TEXT = 1 | |
const T_PATTERN = 2 | |
const T_LIST = 3 | |
const patternRe = /\[(?<marker>\+|\/|:)(?<id>[A-Za-z0-9_-]+)\]/y | |
const listRe = /\[(?<id>[A-Za-z0-9_-]+):(?<list>(\S+(\|\S+)*))\]/y | |
export function parse(value) { | |
const ids = new Set() | |
const tokens = [] | |
let token | |
let i = 0 | |
while (i < value.length) { | |
let capturedLength = 1 | |
patternRe.lastIndex = i | |
listRe.lastIndex = i | |
let match | |
if ((match = value.match(patternRe))) { | |
const {groups} = match | |
tokens.push(createToken(T_PATTERN, i, {...groups})) | |
if (ids.has(groups.id)) { | |
throw new Error(`Id "${groups.id}" at ${i + 1} is already taken`) | |
} | |
ids.add(groups.id) | |
token = null | |
capturedLength = match[0].length | |
} else if ((match = value.match(listRe))) { | |
const {groups} = match | |
tokens.push( | |
createToken(T_LIST, i, { | |
id: groups.id, | |
list: groups.list.split('|'), | |
}), | |
) | |
if (ids.has(groups.id)) { | |
throw new Error(`Id "${groups.id}" at ${i + 1} is already taken`) | |
} | |
ids.add(groups.id) | |
token = null | |
capturedLength = match[0].length | |
} else { | |
if (!token) { | |
token = createToken(T_TEXT, i, value[i]) | |
tokens.push(token) | |
} else { | |
token.value += value[i] | |
} | |
} | |
i += capturedLength | |
} | |
return tokens | |
} | |
function createToken(type, index, value) { | |
return { | |
type, | |
index, | |
value, | |
} | |
} | |
export function createRegexp(tokens) { | |
let parts = [] | |
for (const token of tokens) { | |
if (token.type === T_TEXT) { | |
parts.push(escapeRegex(token.value)) | |
} else if (token.type === T_PATTERN) { | |
const {marker, id} = token.value | |
parts.push(`(?<${id}>`) | |
switch (marker) { | |
case '+': { | |
parts.push('\\d+') | |
break | |
} | |
case '/': { | |
parts.push('\\d+(\\.\\d+)?') | |
break | |
} | |
case ':': { | |
parts.push('\\S+') | |
break | |
} | |
} | |
parts.push(')') | |
} else if (token.type === T_LIST) { | |
const {list, id} = token.value | |
parts.push(`(?<${id}>${list.map(v => escapeRegex(v)).join('|')})`) | |
} else { | |
throw new Error(`Unknown token type: "${token.type}"`) | |
} | |
} | |
return new RegExp(`^${parts.join('')}$`) | |
} | |
function extractInteger(v) { | |
return parseInt(v, 10) | |
} | |
function extractFloat(v) { | |
if (v.includes('.')) { | |
return parseFloat(v) | |
} else { | |
return parseInt(v, 10) | |
} | |
} | |
function extractSlug(v) { | |
return v | |
} | |
export function createExtractor(tokens) { | |
const map = {} | |
for (const token of tokens) { | |
if (token.type === T_PATTERN) { | |
const {id, marker} = token.value | |
switch (marker) { | |
case '+': { | |
map[id] = extractInteger | |
break | |
} | |
case '/': { | |
map[id] = extractFloat | |
break | |
} | |
case ':': { | |
map[id] = extractSlug | |
break | |
} | |
default: | |
throw new Error(`Unknown marker "${marker}".`) | |
} | |
} else if (token.type === T_LIST) { | |
const {id} = token.value | |
map[id] = extractSlug | |
} | |
} | |
return groups => { | |
const result = {} | |
for (const [k, v] of Object.entries(groups)) { | |
if (k in map) { | |
result[k] = map[k](v) | |
} | |
} | |
return result | |
} | |
} | |
export function createRouteMatcher(rule) { | |
const tokens = parse(rule) | |
if (tokens.length === 1 && tokens[0].type === T_TEXT) { | |
return null | |
} | |
const regex = createRegexp(tokens) | |
const extract = createExtractor(tokens) | |
return { | |
regex, | |
match: v => v.length && v[0].match(regex), | |
extract, | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment