Skip to content

Instantly share code, notes, and snippets.

@rumkin
Created May 31, 2020 13:58
Show Gist options
  • Save rumkin/ae8b2ec9c12de4fa4ceec89ff8b54449 to your computer and use it in GitHub Desktop.
Save rumkin/ae8b2ec9c12de4fa4ceec89ff8b54449 to your computer and use it in GitHub Desktop.
Parse routes for CSS props
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