Skip to content

Commit

Permalink
Refactor with utility functions
Browse files Browse the repository at this point in the history
  • Loading branch information
kjellmorten committed Sep 23, 2017
1 parent 22fed1f commit 5cdedc0
Show file tree
Hide file tree
Showing 7 changed files with 415 additions and 74 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ be excluded from the resulting uri. With the `type` parameter set to `entry` and
an undefined `id` parameter, this template would expand to the uri
`http://example.com/entry/`.

To include curly brackets in the url, without replacement, escape the opening
bracket: `?brackets=\\{}`. (Remember double escape characters to escape the
escape character in JavaScript. :)
To include curly brackets in the url, without replacement, simply escape them:
`?brackets=\\{\\}`. (Remember double escape characters to escape the
escape character in JavaScript.)

### Query parameters

Expand Down
10 changes: 9 additions & 1 deletion lib/compile-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ test('should compile optional query component', (t) => {

test('should compile query continuation', (t) => {
const template = 'http://example.com/?id=ent1{&first,max}'
const expected = ['http://example.com/?id=ent1', ['&', {param: 'first'}, {param: 'max'}]]
const expected = ['http://example.com/', '?id=ent1', ['&', {param: 'first'}, {param: 'max'}]]

const ret = compile(template)

Expand Down Expand Up @@ -270,3 +270,11 @@ test('should compile filter function with equal sign in argument', (t) => {

t.deepEqual(ret, expected)
})

test('should throw for unclosed parameter', (t) => {
const template = 'http://example.com/{type'

t.throws(() => {
compile(template)
})
})
132 changes: 62 additions & 70 deletions lib/compile.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,24 @@
const filters = require('./filters')
const splitTemplate = require('./utils/splitTemplate')
const splitComponent = require('./utils/splitComponent')

const split = (template) => {
const array = []
const delimiters = /[,()]/g
let paran = false
let from = 0
let match

while ((match = delimiters.exec(template)) !== null) {
switch (match[0]) {
case '(':
paran = true
break
case ')':
paran = false
break
case ',':
if (!paran && template[delimiters.lastIndex - 2] !== '\\') {
array.push(template.substring(from, delimiters.lastIndex - 1).trim())
from = delimiters.lastIndex
}
}
const fixEscapedCommas = (args, arg, index, array) => {
if (arg.substr(arg.length - 1) === '\\' && array.length > index) {
const next = array.splice(index + 1, 1)
return args.concat(arg.substr(0, arg.length - 1) + ',' + next[0])
}

array.push(template.substring(from).trim())
return array
return args.concat(arg)
}

const compileParam = (explode) => (segment) => {
const {length} = segment

// Optional or not
const obj = (segment.substr(length - 1) === '?')
? {
param: segment.substr(0, length - 1),
optional: true
}
: {param: segment}
const setOptional = (obj) => {
const {length} = obj.param
if (obj.param.substr(length - 1) === '?') {
obj.param = obj.param.substr(0, length - 1)
obj.optional = true
}
}

// Filter functions
const setFilterFunctions = (obj) => {
const filterParts = obj.param.split('|')
if (filterParts.length > 1) {
obj.param = filterParts[0]
Expand All @@ -47,66 +27,77 @@ const compileParam = (explode) => (segment) => {
const match = /^(\w+)\(([^)]*)\)$/.exec(filter)
const obj = {function: filters[(match && match.length > 1) ? match[1] : filter]}
if (match && match[2]) {
obj.args = split(match[2]).map((filter) => filter.replace('\\,', ','))
obj.args = match[2].split(',')
.reduce(fixEscapedCommas, [])
.map((arg) => arg.trim())
}
return obj
})
}
}

// Prefix modifier
const prefixParts = obj.param.split(':')
if (prefixParts.length > 1) {
obj.param = prefixParts[0]
obj.filters = [{function: filters.max, args: [prefixParts[1]]}].concat(obj.filters || [])
const setPrefixFunction = (obj) => {
const index = obj.param.indexOf(':')
if (index > -1) {
const key = obj.param.substr(index + 1)
obj.filters = [{function: filters.max, args: [key]}]
.concat(obj.filters || [])
obj.param = obj.param.substr(0, index)
}
}

// Specified key
const setKey = (obj) => {
const keyParts = obj.param.split('=')
if (keyParts.length > 1) {
obj.key = keyParts[0]
obj.param = keyParts[1]
}
}

const compileParam = (explode = false) => (param, index) => {
if (index === 0) {
// Skip modifier
return param
}

const obj = {param}

setOptional(obj)
setFilterFunctions(obj)
setPrefixFunction(obj)
setKey(obj)

// Explode
if (explode) {
obj.explode = true
}

return obj
}

const compileComponent = (segment) => {
const modifier = (/^[?&/#.;]/.test(segment)) ? segment.substr(0, 1) : null
const list = split(segment.substr(modifier ? 1 : 0))
const explode = (modifier === '/' || modifier === '.')
if (modifier || list.length > 1) {
return [modifier].concat(list.map(compileParam(explode)))
const compileComponent = (component) => {
const parts = splitComponent(component)
if (parts.length < 2) {
throw new TypeError(`Can't compile template with parameter '${component}'`)
}
return compileParam(explode)(segment)
}

const prepareSegment = (segment) => segment.replace(/\\([{}])/g, '$1')
const modifier = parts[0]

const compileSegment = (template, open = true) => {
let index = -1
do {
index = template.indexOf((open) ? '{' : '}', index + 1)
} while (template[index - 1] === '\\')

if (index === -1) {
return (template) ? [prepareSegment(template)] : []
if (modifier === null && parts.length === 2) {
return compileParam()(parts[1])
}

let element = prepareSegment(template.substr(0, index))
if (!open) {
element = compileComponent(element)
}
const explode = (modifier === '/' || modifier === '.')
return parts.map(compileParam(explode))
}

const compileSegment = (segment) => {
const isComponent = segment[0] === '{'
segment = segment.replace(/\\([{}])/g, '$1')

const rest = compileSegment(template.substr(index + 1), !open)
if (element !== '') {
return [element, ...rest]
if (isComponent) {
return compileComponent(segment)
} else {
return segment
}
return rest
}

/**
Expand All @@ -115,7 +106,8 @@ const compileSegment = (template, open = true) => {
* @returns {array} An array of template segments/components
*/
function compile (template) {
return compileSegment(template)
const segments = splitTemplate(template)
return segments.map(compileSegment)
}

module.exports = compile
115 changes: 115 additions & 0 deletions lib/utils/splitComponent-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import test from 'ava'

import splitComponent from './splitComponent'

test('should exist', (t) => {
t.is(typeof splitComponent, 'function')
})

test('should return empty array when no component', (t) => {
const component = null
const expected = []

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should return empty array when no parameters', (t) => {
const component = '{}'
const expected = []

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split component into modifier and params', (t) => {
const component = '{id}'
const expected = [null, 'id']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split several params', (t) => {
const component = '{first,max}'
const expected = [null, 'first', 'max']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with query modifier', (t) => {
const component = '{?first,max}'
const expected = ['?', 'first', 'max']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with query continuation modifier', (t) => {
const component = '{&first,max}'
const expected = ['&', 'first', 'max']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with path modifier', (t) => {
const component = '{/first,max}'
const expected = ['/', 'first', 'max']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with fragment modifier', (t) => {
const component = '{#first,max}'
const expected = ['#', 'first', 'max']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with dot modifier', (t) => {
const component = '{.first,max}'
const expected = ['.', 'first', 'max']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with path-style modifier', (t) => {
const component = '{;first,max}'
const expected = [';', 'first', 'max']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with filter function', (t) => {
const component = '{/section|max(3),user}'
const expected = ['/', 'section|max(3)', 'user']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})

test('should split with filter function several args', (t) => {
const component = '{/section,type|map(entry=entries,article=articles)}'
const expected = ['/', 'section', 'type|map(entry=entries,article=articles)']

const ret = splitComponent(component)

t.deepEqual(ret, expected)
})
40 changes: 40 additions & 0 deletions lib/utils/splitComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const split = (component) => {
const array = []
const regex = /[,()]/g
let paran = false
let from = 0
let match

while ((match = regex.exec(component)) !== null) {
switch (match[0]) {
case '(':
paran = true
break
case ')':
paran = false
break
case ',':
if (!paran) {
array.push(component.substring(from, regex.lastIndex - 1).trim())
from = regex.lastIndex
}
}
}

array.push(component.substring(from).trim())

return array
}

function splitComponent (component) {
const match = /^\{([?&/#.;]?).+\}$/.exec(component)
if (!match) {
return []
}
const modifier = match[1] || null
component = component.substring((modifier) ? 2 : 1, component.length - 1)

return [modifier, ...split(component)]
}

module.exports = splitComponent
Loading

0 comments on commit 5cdedc0

Please sign in to comment.