@financial-times/dotcom-server-handlebars 7.3.1 → 7.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/PageKitHandlebars.ts +142 -0
- package/src/__test__/HandlebarsRenderer.spec.ts +99 -0
- package/src/__test__/__fixtures__/components/DefaultExportComponentCJS.js +5 -0
- package/src/__test__/__fixtures__/components/DefaultExportComponentES.tsx +5 -0
- package/src/__test__/__fixtures__/components/NamedExportComponentCJS.js +5 -0
- package/src/__test__/__fixtures__/components/NamedExportComponentES.tsx +5 -0
- package/src/__test__/__fixtures__/views/partials/content.hbs +1 -0
- package/src/__test__/__fixtures__/views/partials/partial.hbs +1 -0
- package/src/__test__/__fixtures__/views/partials/wrapper.hbs +3 -0
- package/src/__test__/__fixtures__/views/view.hbs +4 -0
- package/src/__test__/helpers.spec.ts +341 -0
- package/src/findPartialFiles.ts +25 -0
- package/src/helpers/README.md +213 -0
- package/src/helpers/array.ts +8 -0
- package/src/helpers/capture.ts +9 -0
- package/src/helpers/concat.ts +8 -0
- package/src/helpers/dateformat.ts +13 -0
- package/src/helpers/encode.ts +15 -0
- package/src/helpers/ifAll.ts +12 -0
- package/src/helpers/ifEquals.ts +12 -0
- package/src/helpers/ifEqualsSome.ts +13 -0
- package/src/helpers/ifSome.ts +12 -0
- package/src/helpers/json.ts +16 -0
- package/src/helpers/renderReactComponent.ts +34 -0
- package/src/helpers/resize.ts +20 -0
- package/src/helpers/slice.ts +22 -0
- package/src/helpers/unlessAll.ts +12 -0
- package/src/helpers/unlessEquals.ts +12 -0
- package/src/helpers/unlessSome.ts +12 -0
- package/src/helpers.ts +16 -0
- package/src/index.ts +3 -0
- package/src/loadFileContents.ts +5 -0
- package/src/types.d.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@financial-times/dotcom-server-handlebars",
|
|
3
|
-
"version": "7.3.
|
|
3
|
+
"version": "7.3.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/node/index.js",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"npm": "7.x || 8.x"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
|
-
"dist/"
|
|
34
|
+
"dist/",
|
|
35
|
+
"src/"
|
|
35
36
|
],
|
|
36
37
|
"repository": {
|
|
37
38
|
"type": "git",
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import mixinDeep from 'mixin-deep'
|
|
3
|
+
import Handlebars, { HelperDelegate, TemplateDelegate } from 'handlebars'
|
|
4
|
+
import findPartialFiles from './findPartialFiles'
|
|
5
|
+
import loadFileContents from './loadFileContents'
|
|
6
|
+
import { TRenderCallback, TPartialTemplates, TFilePaths } from './types'
|
|
7
|
+
|
|
8
|
+
export type TPageKitHandlebarsOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* An instance of Handlebars to use.
|
|
11
|
+
* @default require('handlebars')
|
|
12
|
+
*/
|
|
13
|
+
handlebars?: typeof Handlebars
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Current working directory.
|
|
17
|
+
* @default process.cwd()
|
|
18
|
+
*/
|
|
19
|
+
rootDirectory?: string
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Additional helper functions to register with Handlebars.
|
|
23
|
+
* @default {}
|
|
24
|
+
*/
|
|
25
|
+
helpers?: {
|
|
26
|
+
[key: string]: HelperDelegate
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Partial templates to register with Handlebars.
|
|
31
|
+
* @default {}
|
|
32
|
+
*/
|
|
33
|
+
partials?: TPartialTemplates
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A list of directories and patterns used to dynamically find and load partial template files.
|
|
37
|
+
* @default { './views/partials': '**\/*.{hbs,html}' }
|
|
38
|
+
*/
|
|
39
|
+
partialPaths?: TFilePaths
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Enable caching of template files to reduce filesystem I/O
|
|
43
|
+
* @default process.env.NODE_ENV !== 'development
|
|
44
|
+
*/
|
|
45
|
+
cache?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// By default NODE_ENV will be undefined so be explicit
|
|
49
|
+
const nodeEnv = process.env.NODE_ENV || 'development'
|
|
50
|
+
|
|
51
|
+
const defaultOptions: TPageKitHandlebarsOptions = {
|
|
52
|
+
cache: nodeEnv !== 'development',
|
|
53
|
+
rootDirectory: process.cwd(),
|
|
54
|
+
helpers: {},
|
|
55
|
+
partials: {},
|
|
56
|
+
partialPaths: {
|
|
57
|
+
'./views/partials': '**/*.{hbs,html}',
|
|
58
|
+
'./bower_components': '*/{templates,server/templates,components,partials,views}/**/*.{hbs,html}',
|
|
59
|
+
'./node_modules/@financial-times':
|
|
60
|
+
'*/{templates,server/templates,components,partials,views}/**/*.{hbs,html}'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class PageKitHandlebars {
|
|
65
|
+
public options: TPageKitHandlebarsOptions
|
|
66
|
+
public engine: this['renderView']
|
|
67
|
+
|
|
68
|
+
private cache: Map<string, any> = new Map()
|
|
69
|
+
|
|
70
|
+
constructor(userOptions: TPageKitHandlebarsOptions = {}) {
|
|
71
|
+
this.options = mixinDeep({}, defaultOptions, userOptions)
|
|
72
|
+
|
|
73
|
+
// Create a point for Express to mount as a view engine
|
|
74
|
+
this.engine = this.renderView.bind(this)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
loadPartials(): TPartialTemplates {
|
|
78
|
+
let partials: TFilePaths = this.cache.get('__partials__')
|
|
79
|
+
|
|
80
|
+
if (partials === undefined) {
|
|
81
|
+
partials = findPartialFiles(this.options.rootDirectory, this.options.partialPaths)
|
|
82
|
+
|
|
83
|
+
if (this.options.cache) {
|
|
84
|
+
this.cache.set('__partials__', partials)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const templates = {}
|
|
89
|
+
|
|
90
|
+
Object.keys(partials).forEach((name) => {
|
|
91
|
+
const filePath = partials[name]
|
|
92
|
+
templates[name] = this.loadTemplate(filePath)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
return templates
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
loadTemplate(filePath: string): TemplateDelegate {
|
|
99
|
+
if (path.isAbsolute(filePath) === false) {
|
|
100
|
+
filePath = path.resolve(this.options.rootDirectory, filePath)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let template: TemplateDelegate = this.cache.get(filePath)
|
|
104
|
+
|
|
105
|
+
if (template === undefined) {
|
|
106
|
+
const contents = loadFileContents(filePath)
|
|
107
|
+
const hbs = this.options.handlebars || Handlebars
|
|
108
|
+
|
|
109
|
+
template = hbs.compile(contents)
|
|
110
|
+
|
|
111
|
+
if (this.options.cache) {
|
|
112
|
+
this.cache.set(filePath, template)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return template
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
render(template: string | TemplateDelegate, templateContext: any): string {
|
|
120
|
+
if (typeof template === 'string') {
|
|
121
|
+
template = this.loadTemplate(template)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const html = template(templateContext, {
|
|
125
|
+
helpers: this.options.helpers,
|
|
126
|
+
partials: { ...this.options.partials, ...this.loadPartials() }
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return html.trim()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// This method is intended to be mounted by Express and used as a view engine.
|
|
133
|
+
// <https://expressjs.com/en/guide/using-template-engines.html>
|
|
134
|
+
renderView(templatePath: string, templateContext: any, callback: TRenderCallback): void {
|
|
135
|
+
try {
|
|
136
|
+
const html = this.render(templatePath, templateContext)
|
|
137
|
+
callback(null, html)
|
|
138
|
+
} catch (error) {
|
|
139
|
+
callback(error)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import { PageKitHandlebars as Subject } from '../PageKitHandlebars'
|
|
3
|
+
|
|
4
|
+
// NOTE: Tests are run from the repository root directory so we need to set the CWD
|
|
5
|
+
const root = path.join(__dirname, '__fixtures__')
|
|
6
|
+
const view = path.resolve(root, 'views/view.hbs')
|
|
7
|
+
|
|
8
|
+
describe('dotcom-server-handlebars/src/PageKitHandlebars', () => {
|
|
9
|
+
let instance
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
instance = new Subject({
|
|
13
|
+
rootDirectory: root,
|
|
14
|
+
cache: true
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('.loadPartials()', () => {
|
|
19
|
+
let result
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
result = instance.loadPartials()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('loads and compiles partial templates from disk', () => {
|
|
26
|
+
const values = Object.values(result)
|
|
27
|
+
|
|
28
|
+
expect(values.length).toBe(3)
|
|
29
|
+
|
|
30
|
+
values.forEach((template) => {
|
|
31
|
+
expect(template).toEqual(expect.any(Function))
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('caches each compiled partial template', () => {
|
|
36
|
+
const keys = Object.keys(result)
|
|
37
|
+
|
|
38
|
+
keys.forEach((key) => {
|
|
39
|
+
const filename = path.join(root, `views/partials/${key}.hbs`)
|
|
40
|
+
expect(instance.cache.has(filename)).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('.loadTemplate()', () => {
|
|
46
|
+
let result
|
|
47
|
+
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
result = instance.loadTemplate(view)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('loads and compiles a template from disk', () => {
|
|
53
|
+
expect(result).toEqual(expect.any(Function))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('caches the compiled template', () => {
|
|
57
|
+
expect(instance.cache.has(view)).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('.render()', () => {
|
|
62
|
+
const templateContext = { title: 'Hello World', aside: 'Lorem ipsum' }
|
|
63
|
+
|
|
64
|
+
it('can render a template from disk', () => {
|
|
65
|
+
const result = instance.render(view, templateContext)
|
|
66
|
+
|
|
67
|
+
expect(result).toContain('<h1>Hello World</h1>')
|
|
68
|
+
expect(result).toContain('<aside>Lorem ipsum</aside>')
|
|
69
|
+
expect(result).toMatch(/<main>.+<\/main>/s)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('can render a given template function', () => {
|
|
73
|
+
const template = instance.loadTemplate(view)
|
|
74
|
+
const result = instance.render(template, templateContext)
|
|
75
|
+
|
|
76
|
+
expect(result).toContain('<h1>Hello World</h1>')
|
|
77
|
+
expect(result).toContain('<aside>Lorem ipsum</aside>')
|
|
78
|
+
expect(result).toMatch(/<main>.+<\/main>/s)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
describe('.renderView()', () => {
|
|
83
|
+
it('can render a template and fire a callback with the result', () => {
|
|
84
|
+
return new Promise((done) => {
|
|
85
|
+
const templateContext = { title: 'Hello World', aside: 'Lorem ipsum' }
|
|
86
|
+
|
|
87
|
+
instance.renderView(view, templateContext, (error, result) => {
|
|
88
|
+
expect(error).toBeNull()
|
|
89
|
+
|
|
90
|
+
expect(result).toContain('<h1>Hello World</h1>')
|
|
91
|
+
expect(result).toContain('<aside>Lorem ipsum</aside>')
|
|
92
|
+
expect(result).toMatch(/<main>.+<\/main>/s)
|
|
93
|
+
|
|
94
|
+
done()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<h1>{{title}}</h1>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<aside>{{aside}}</aside>
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import { compile } from 'handlebars'
|
|
2
|
+
import * as helpers from '../helpers'
|
|
3
|
+
|
|
4
|
+
describe('dotcom-server-handlebars/src/helpers', () => {
|
|
5
|
+
describe('block helpers', () => {
|
|
6
|
+
describe('#capture', () => {
|
|
7
|
+
it('captures the string and assigns it to a variable', () => {
|
|
8
|
+
const template = compile('{{#capture "myOutput"}}Hello, World!{{/capture}}')
|
|
9
|
+
const templateData = {} as { [key: string]: any }
|
|
10
|
+
const result = template(templateData, { helpers })
|
|
11
|
+
|
|
12
|
+
expect(templateData.myOutput).toBe('Hello, World!')
|
|
13
|
+
expect(result).toBe('')
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('#dateformat', () => {
|
|
18
|
+
const date = new Date('2019-04-10 13:40:21 GMT+0100 (BST)')
|
|
19
|
+
|
|
20
|
+
it('formats as ISO date time by default', () => {
|
|
21
|
+
const template = compile('{{#dateformat}}{{date}}{{/dateformat}}')
|
|
22
|
+
const result = template({ date }, { helpers })
|
|
23
|
+
|
|
24
|
+
expect(result).toBe('2019-04-10T12:40:21Z')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('formats using the given format', () => {
|
|
28
|
+
const template = compile('{{#dateformat "dddd, mmmm d, yyyy"}}{{date}}{{/dateformat}}')
|
|
29
|
+
const result = template({ date }, { helpers })
|
|
30
|
+
|
|
31
|
+
expect(result).toBe('Wednesday, April 10, 2019')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
35
|
+
const template = compile('{{#dateformat "" ""}}{{date}}{{/dateformat}}')
|
|
36
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe('#ifAll', () => {
|
|
41
|
+
const template = compile('{{#ifAll foo bar baz}}yes{{else}}no{{/ifAll}}')
|
|
42
|
+
|
|
43
|
+
it('outputs the contents when all parameters are truthy', () => {
|
|
44
|
+
const result = template({ foo: 123, bar: 'abc', baz: true }, { helpers })
|
|
45
|
+
expect(result).toBe('yes')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('does not output the contents if any parameters are falsy', () => {
|
|
49
|
+
const result = template({ foo: 123, bar: 'abc', baz: false }, { helpers })
|
|
50
|
+
expect(result).toBe('no')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
54
|
+
const template = compile('{{#ifSome}}yes{{/ifSome}}')
|
|
55
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('#ifEquals', () => {
|
|
60
|
+
const template = compile('{{#ifEquals foo bar}}yes{{else}}no{{/ifEquals}}')
|
|
61
|
+
|
|
62
|
+
it('outputs the contents parameters are strictly equal', () => {
|
|
63
|
+
const result = template({ foo: true, bar: true }, { helpers })
|
|
64
|
+
expect(result).toBe('yes')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('does not output the contents if any parameter is not strictly equal', () => {
|
|
68
|
+
const result = template({ foo: true, bar: null }, { helpers })
|
|
69
|
+
expect(result).toBe('no')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
73
|
+
const template = compile('{{#ifEquals}}yes{{/ifEquals}}')
|
|
74
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('#ifEqualsSome', () => {
|
|
79
|
+
const template = compile('{{#ifEqualsSome foo bar baz}}yes{{else}}no{{/ifEqualsSome}}')
|
|
80
|
+
|
|
81
|
+
it('outputs the contents if parameters are strictly equal', () => {
|
|
82
|
+
const result = template({ foo: true, bar: true, baz: true }, { helpers })
|
|
83
|
+
expect(result).toBe('yes')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('outputs true if first parameter is equal', () => {
|
|
87
|
+
const result = template({ foo: true, bar: false, baz: true }, { helpers })
|
|
88
|
+
expect(result).toBe('yes')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('outputs true if second parameter is equal', () => {
|
|
92
|
+
const result = template({ foo: true, bar: true, baz: false }, { helpers })
|
|
93
|
+
expect(result).toBe('yes')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('outputs false if neither parameter is equal', () => {
|
|
97
|
+
const result = template({ foo: true, bar: false, baz: false }, { helpers })
|
|
98
|
+
expect(result).toBe('no')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
102
|
+
const template = compile('{{#ifEqualsSome}}yes{{/ifEqualsSome}}')
|
|
103
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
describe('#ifSome', () => {
|
|
108
|
+
const template = compile('{{#ifSome foo bar baz}}yes{{else}}no{{/ifSome}}')
|
|
109
|
+
|
|
110
|
+
it('outputs the contents if at least one parameter is truthy', () => {
|
|
111
|
+
const result = template({ foo: 123, bar: '', baz: false }, { helpers })
|
|
112
|
+
expect(result).toBe('yes')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('does not output the contents if all parameters are falsy', () => {
|
|
116
|
+
const result = template({ foo: 0, bar: '', baz: false }, { helpers })
|
|
117
|
+
expect(result).toBe('no')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
121
|
+
const template = compile('{{#ifSome}}yes{{/ifSome}}')
|
|
122
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('#renderReactComponent', () => {
|
|
127
|
+
it('renders the specified component (exported as a default CJS Module export) from a local path', () => {
|
|
128
|
+
const template = compile(`{{{renderReactComponent
|
|
129
|
+
localPath="packages/dotcom-server-handlebars/src/__test__/__fixtures__/components/DefaultExportComponentCJS"
|
|
130
|
+
text="foo"
|
|
131
|
+
}}}`)
|
|
132
|
+
const result = template({}, { helpers })
|
|
133
|
+
|
|
134
|
+
expect(result).toBe('<div>foo</div>')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('renders the specified component (exported as a default ES Module export) from a local path', () => {
|
|
138
|
+
const template = compile(`{{{renderReactComponent
|
|
139
|
+
localPath="packages/dotcom-server-handlebars/src/__test__/__fixtures__/components/DefaultExportComponentES"
|
|
140
|
+
text="bar"
|
|
141
|
+
}}}`)
|
|
142
|
+
const result = template({}, { helpers })
|
|
143
|
+
|
|
144
|
+
expect(result).toBe('<div>bar</div>')
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('renders the specified component (exported as a named CJS Module export) from a local path', () => {
|
|
148
|
+
const template = compile(`{{{renderReactComponent
|
|
149
|
+
localPath="packages/dotcom-server-handlebars/src/__test__/__fixtures__/components/NamedExportComponentCJS"
|
|
150
|
+
namedExport="Component"
|
|
151
|
+
text="baz"
|
|
152
|
+
}}}`)
|
|
153
|
+
const result = template({}, { helpers })
|
|
154
|
+
|
|
155
|
+
expect(result).toBe('<div>baz</div>')
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('renders the specified component (exported as a named ES Module export) from a local path', () => {
|
|
159
|
+
const template = compile(`{{{renderReactComponent
|
|
160
|
+
localPath="packages/dotcom-server-handlebars/src/__test__/__fixtures__/components/NamedExportComponentES"
|
|
161
|
+
namedExport="Component"
|
|
162
|
+
text="qux"
|
|
163
|
+
}}}`)
|
|
164
|
+
const result = template({}, { helpers })
|
|
165
|
+
|
|
166
|
+
expect(result).toBe('<div>qux</div>')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('throws if mandatory parameters are not provided', () => {
|
|
170
|
+
const template = compile('{{{renderReactComponent}}}')
|
|
171
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('#resize', () => {
|
|
176
|
+
it('formats the image as an image service URL', () => {
|
|
177
|
+
const template = compile('{{#resize 640}}http://website.com/picture.jpg{{/resize}}')
|
|
178
|
+
const result = template({}, { helpers })
|
|
179
|
+
|
|
180
|
+
expect(result).toBe(
|
|
181
|
+
'https://www.ft.com/__origami/service/image/v2/images/raw/http%3A%2F%2Fwebsite.com%2Fpicture.jpg?width=640&source=next&fit=scale-down'
|
|
182
|
+
)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('accepts named parameters', () => {
|
|
186
|
+
const template = compile('{{#resize 640 fit="contain"}}http://website.com/picture.jpg{{/resize}}')
|
|
187
|
+
const result = template({}, { helpers })
|
|
188
|
+
|
|
189
|
+
expect(result).toBe(
|
|
190
|
+
'https://www.ft.com/__origami/service/image/v2/images/raw/http%3A%2F%2Fwebsite.com%2Fpicture.jpg?width=640&source=next&fit=contain'
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
195
|
+
const template = compile('{{#resize}}http://website.com/picture.jpg{{/resize}}')
|
|
196
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('#slice', () => {
|
|
201
|
+
it('slices a given array and iterates over the new array', () => {
|
|
202
|
+
const template = compile('{{#slice items offset="2" limit="3"}}{{this}}{{/slice}}')
|
|
203
|
+
const result = template({ items: [1, 2, 3, 4, 5, 6] }, { helpers })
|
|
204
|
+
|
|
205
|
+
expect(result).toBe('345')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
209
|
+
const template = compile('{{#slice offset="2" limit="3"}}{{this}}{{/slice}}')
|
|
210
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
describe('#unlessAll', () => {
|
|
215
|
+
const template = compile('{{#unlessAll foo bar baz}}yes{{else}}no{{/unlessAll}}')
|
|
216
|
+
|
|
217
|
+
it('outputs the contents if all parameters are falsy', () => {
|
|
218
|
+
const result = template({ foo: 0, bar: '', baz: false }, { helpers })
|
|
219
|
+
expect(result).toBe('yes')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it('does not output the contents if any parameters are truthy', () => {
|
|
223
|
+
const result = template({ foo: 0, bar: '', baz: true }, { helpers })
|
|
224
|
+
expect(result).toBe('no')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
228
|
+
const template = compile('{{#unlessAll}}yes{{/unlessAll}}')
|
|
229
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
describe('#unlessEquals', () => {
|
|
234
|
+
const template = compile('{{#unlessEquals foo bar}}yes{{else}}no{{/unlessEquals}}')
|
|
235
|
+
|
|
236
|
+
it('outputs the contents if all parameters are falsy', () => {
|
|
237
|
+
const result = template({ foo: true, bar: false }, { helpers })
|
|
238
|
+
expect(result).toBe('yes')
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('does not output the contents if any parameters are truthy', () => {
|
|
242
|
+
const result = template({ foo: true, bar: true }, { helpers })
|
|
243
|
+
expect(result).toBe('no')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
247
|
+
const template = compile('{{#unlessEquals}}yes{{/unlessEquals}}')
|
|
248
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
describe('#unlessSome', () => {
|
|
253
|
+
const template = compile('{{#unlessSome foo bar baz}}yes{{else}}no{{/unlessSome}}')
|
|
254
|
+
|
|
255
|
+
it('outputs the contents if any parameters are falsy', () => {
|
|
256
|
+
const result = template({ foo: 1, bar: 'abc', baz: false }, { helpers })
|
|
257
|
+
expect(result).toBe('yes')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it('does not output the contents if all parameters are truthy', () => {
|
|
261
|
+
const result = template({ foo: 1, bar: 'abc', baz: true }, { helpers })
|
|
262
|
+
expect(result).toBe('no')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
266
|
+
const template = compile('{{#unlessSome}}yes{{/unlessSome}}')
|
|
267
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
describe('inline helpers', () => {
|
|
273
|
+
describe('array', () => {
|
|
274
|
+
it('converts all parameters into one array', () => {
|
|
275
|
+
const template = compile('{{array foo bar baz}}')
|
|
276
|
+
const result = template({ foo: 1, bar: 2, baz: 3 }, { helpers })
|
|
277
|
+
|
|
278
|
+
expect(result).toBe('1,2,3')
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
282
|
+
const template = compile('{{array}}')
|
|
283
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
284
|
+
})
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
describe('concat', () => {
|
|
288
|
+
it('concatenates all parameters into one string', () => {
|
|
289
|
+
const template = compile('{{concat "Welcome to " place ", " name}}')
|
|
290
|
+
const result = template({ place: 'Hell', name: 'human' }, { helpers })
|
|
291
|
+
|
|
292
|
+
expect(result).toBe('Welcome to Hell, human')
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
296
|
+
const template = compile('{{concat}}')
|
|
297
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
298
|
+
})
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe('encode', () => {
|
|
302
|
+
it('encodes the parameter as a URI component', () => {
|
|
303
|
+
const template = compile('{{{encode title}}}')
|
|
304
|
+
const result = template({ title: 'http://www.foo.com?bar=baz&qux=«»' }, { helpers })
|
|
305
|
+
|
|
306
|
+
expect(result).toBe('http%3A%2F%2Fwww.foo.com%3Fbar%3Dbaz%26qux%3D%C2%AB%C2%BB')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('encodes the parameter as a complete URI', () => {
|
|
310
|
+
const template = compile('{{{encode title mode="uri"}}}')
|
|
311
|
+
const result = template({ title: 'http://www.foo.com?bar=baz&qux=«»' }, { helpers })
|
|
312
|
+
|
|
313
|
+
expect(result).toBe('http://www.foo.com?bar=baz&qux=%C2%AB%C2%BB')
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
317
|
+
const template = compile('{{encode}}')
|
|
318
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
319
|
+
})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
describe('json', () => {
|
|
323
|
+
it('stringifies the parameter', () => {
|
|
324
|
+
const template = compile('{{{json data}}}')
|
|
325
|
+
const result = template({ data: { foo: 'bar', baz: 123, qux: true } }, { helpers })
|
|
326
|
+
|
|
327
|
+
expect(result).toBe('{"foo":"bar","baz":123,"qux":true}')
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
it('throws if the incorrect number of parameters are provided', () => {
|
|
331
|
+
const template = compile('{{json}}')
|
|
332
|
+
expect(() => template({}, { helpers })).toThrow()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('throws if the user tries to output the @root context', () => {
|
|
336
|
+
const template = compile('{{json data}}')
|
|
337
|
+
expect(() => template({ data: { _locals: true } }, { helpers })).toThrow()
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import glob from 'glob'
|
|
3
|
+
import { TFilePaths } from './types'
|
|
4
|
+
|
|
5
|
+
export default function loadPartialFiles(cwd: string, partialPaths: TFilePaths): TFilePaths {
|
|
6
|
+
const partialFiles = {}
|
|
7
|
+
|
|
8
|
+
Object.keys(partialPaths).forEach((partialPath) => {
|
|
9
|
+
const globPattern = partialPaths[partialPath]
|
|
10
|
+
const baseDirectory = path.resolve(cwd, partialPath)
|
|
11
|
+
|
|
12
|
+
const matchingFiles = glob.sync(globPattern, {
|
|
13
|
+
cwd: baseDirectory
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
matchingFiles.forEach((file) => {
|
|
17
|
+
const extension = path.extname(file)
|
|
18
|
+
const name = file.replace(extension, '')
|
|
19
|
+
|
|
20
|
+
partialFiles[name] = path.join(baseDirectory, file)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return partialFiles
|
|
25
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Handlebars Helpers
|
|
2
|
+
|
|
3
|
+
This package contains a suite of helpers to enable the migration of applications from [n-handlebars] to Page Kit. Not every helper provided by n-handlebars has been ported over; any helpers we could not find usage of, are used only within [n-ui], or have dependencies on other Handlebars components have not been included.
|
|
4
|
+
|
|
5
|
+
[n-ui]: https://github.com/Financial-Times/n-ui/
|
|
6
|
+
[n-handlebars]: https://github.com/Financial-Times/n-handlebars
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
When using this package you can import the `helpers` property and provide it as an option when creating a new `PageKitHandlebars` instance. Please note that helpers will not be appended to the global Handlebars instance.
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { PageKitHandlebars, helpers } = require('@financial-times/dotcom-server-handlebars')
|
|
15
|
+
const renderer = new PageKitHandlebars({ helpers })
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Alternatively if you want to use the helpers with an existing Handlebars instance:
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
const Handlebars = require('handlebars')
|
|
22
|
+
const { helpers } = require('@financial-times/dotcom-server-handlebars')
|
|
23
|
+
|
|
24
|
+
Handlebars.registerHelper(helpers)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Block helpers
|
|
29
|
+
|
|
30
|
+
### capture
|
|
31
|
+
|
|
32
|
+
Captures the string inside of the opening and closing tags and assigns it to a variable.
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
|
|
36
|
+
```hbs
|
|
37
|
+
{{#capture "myOutput"}}I am being captured.{{/capture}}
|
|
38
|
+
|
|
39
|
+
{{myOutput}}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### dateformat
|
|
43
|
+
|
|
44
|
+
Formats a [date object] using the [dateformat] library. If no format is specified it will default to the `isoUtcDateTime` format.
|
|
45
|
+
|
|
46
|
+
Example:
|
|
47
|
+
|
|
48
|
+
```hbs
|
|
49
|
+
{{#dateformat}}{{date}}{{/dateformat}}
|
|
50
|
+
{{#dateformat "fullDate"}}{{ date }}{{/dateformat}}
|
|
51
|
+
{{#dateformat "dddd, d mmmm, yyyy"}}{{ date }}{{/dateformat}}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
[date object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
|
|
55
|
+
[dateformat]: https://www.npmjs.com/package/dateformat
|
|
56
|
+
|
|
57
|
+
### ifAll
|
|
58
|
+
|
|
59
|
+
Outputs the content if all of the parameters are [truthy].
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
|
|
63
|
+
```hbs
|
|
64
|
+
{{#ifAll foo bar baz}}All parameters are truthy{{else}}A parameter is falsy{{/ifAll}}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
[truthy]: https://developer.mozilla.org/en-US/docs/Glossary/Truthy
|
|
68
|
+
|
|
69
|
+
### ifEquals
|
|
70
|
+
|
|
71
|
+
Outputs the content if all of the parameters are [strictly equal]. The first parameter is used as the control and all parameters are tested against it.
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
|
|
75
|
+
```hbs
|
|
76
|
+
{{#ifEquals foo bar}}Parameters are all equal{{else}}A parameter does not match{{/ifEquals}}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
[strictly equal]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness
|
|
80
|
+
|
|
81
|
+
### ifEqualsSome
|
|
82
|
+
|
|
83
|
+
Outputs the content if some of the parameters are [strictly equal]. The first parameter is used as the control and all parameters are tested against it.
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
|
|
87
|
+
```hbs
|
|
88
|
+
{{#ifEqualsSome foo bar baz}}Some parameters are all equal{{else}}No parameters match{{/ifEqualsSome}}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### ifSome
|
|
92
|
+
|
|
93
|
+
Outputs the content if at least one of the parameters is [truthy].
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
|
|
97
|
+
```hbs
|
|
98
|
+
{{#ifSome foo bar baz}}A parameter is truthy{{else}}All parameters are falsy{{/ifSome}}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### renderReactComponent
|
|
102
|
+
|
|
103
|
+
Outputs a JSX component specified by its local path (relative to the root of the app consuming `dotcom-server-handlebars`) or the package name of the node module, with specification of a named export if required, as well as any other props.
|
|
104
|
+
|
|
105
|
+
Example:
|
|
106
|
+
|
|
107
|
+
```hbs
|
|
108
|
+
{{{renderReactComponent localPath="views/components/ComponentWithHbsOutput" title="This is a React component"}}}
|
|
109
|
+
|
|
110
|
+
{{{renderReactComponent package="@financial-times/dotcom-ui-header" namedExport="LogoOnly" variant="large-logo"}}}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### resize
|
|
114
|
+
|
|
115
|
+
Deliver an image via the [Origami Image Service] and resize it to the specified width. Additional named parameters will be appended to the URL query string.
|
|
116
|
+
|
|
117
|
+
Example:
|
|
118
|
+
|
|
119
|
+
```hbs
|
|
120
|
+
<img src="{{#resize 640}}{{image}}{{/resize}}" />
|
|
121
|
+
<img src="{{#resize 640 fit="contain"}}{{image}}{{/resize}}" />
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
[Origami Image Service]: https://www.ft.com/__origami/service/image/v2/
|
|
125
|
+
|
|
126
|
+
### slice
|
|
127
|
+
|
|
128
|
+
Iterate over a subset of items. Accepts an `offset` and `limit` parameter.
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
|
|
132
|
+
```hbs
|
|
133
|
+
{{#slice iterable offset="2"}}{{this}}{{/slice}}
|
|
134
|
+
{{#slice iterable offset="4" limit="2"}}{{this}}{{/slice}}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### unlessAll
|
|
138
|
+
|
|
139
|
+
Outputs the content if all of the parameters are [falsy].
|
|
140
|
+
|
|
141
|
+
Example:
|
|
142
|
+
|
|
143
|
+
```hbs
|
|
144
|
+
{{#unlessAll foo bar baz}}All parameters are falsy{{else}}A parameter is truthy{{/unlessAll}}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
[falsy]: https://developer.mozilla.org/en-US/docs/Glossary/Falsy
|
|
148
|
+
|
|
149
|
+
### unlessEquals
|
|
150
|
+
|
|
151
|
+
Outputs the content if any parameters are _not_ [strictly equal].
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
|
|
155
|
+
```hbs
|
|
156
|
+
{{#unlessEquals foo bar}}Parameters are not equal{{else}}All parameters match{{/unlessEquals}}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### unlessSome
|
|
160
|
+
|
|
161
|
+
Outputs the content if any of the parameters are [falsy].
|
|
162
|
+
|
|
163
|
+
Example:
|
|
164
|
+
|
|
165
|
+
```hbs
|
|
166
|
+
{{#unlessSome foo bar baz}}A parameter is falsy{{else}}All parameters are truthy{{/unlessSome}}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
## Inline helpers
|
|
171
|
+
|
|
172
|
+
### array
|
|
173
|
+
|
|
174
|
+
Converts the given parameters into a single array.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
|
|
178
|
+
```hbs
|
|
179
|
+
{{array foo bar baz}}
|
|
180
|
+
{{>partial parameter=(array foo bar baz)}}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### concat
|
|
184
|
+
|
|
185
|
+
Concatenates multiple parameters into a single string.
|
|
186
|
+
|
|
187
|
+
Example:
|
|
188
|
+
|
|
189
|
+
```hbs
|
|
190
|
+
{{concat "Welcome to " name}}
|
|
191
|
+
{{>partial parameter=(concat "Welcome to " name)}}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### encode
|
|
195
|
+
|
|
196
|
+
Encodes a uniform resource identifer (URI) using [`encodeURIComponent()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent) or [`encodeURI()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI).
|
|
197
|
+
|
|
198
|
+
Example:
|
|
199
|
+
|
|
200
|
+
```hbs
|
|
201
|
+
{{encode text}}
|
|
202
|
+
{{encode text mode="uri"}}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### json
|
|
206
|
+
|
|
207
|
+
JSON stringifies the given parameter. Please note that this will error if you try to output the `@root` context when used in an Express application as this may contain secret information.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
|
|
211
|
+
```hbs
|
|
212
|
+
{{{json data}}}
|
|
213
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function capture(name: string, options: HelperOptions) {
|
|
4
|
+
if (this.hasOwnProperty(name)) {
|
|
5
|
+
throw Error(`Template data property "${name}" has already been declared.`)
|
|
6
|
+
} else {
|
|
7
|
+
this[name] = options.fn(this)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import formatter from 'dateformat'
|
|
2
|
+
import { HelperOptions } from 'handlebars'
|
|
3
|
+
|
|
4
|
+
export default function dateformat(...args) {
|
|
5
|
+
if (args.length > 2) {
|
|
6
|
+
throw Error('Incorrect number of parameters provided')
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const options = args.pop() as HelperOptions
|
|
10
|
+
const format = args[0] || 'isoUtcDateTime'
|
|
11
|
+
|
|
12
|
+
return formatter(options.fn(this), format)
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function encode(...args) {
|
|
4
|
+
if (args.length !== 2) {
|
|
5
|
+
throw Error('Incorrect number of parameters provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
|
|
10
|
+
if (options.hash.mode === 'uri') {
|
|
11
|
+
return encodeURI(args[0])
|
|
12
|
+
} else {
|
|
13
|
+
return encodeURIComponent(args[0])
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function ifAll(...args) {
|
|
4
|
+
if (args.length < 2) {
|
|
5
|
+
throw Error('At least one parameter must be provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
const condition = args.every(Boolean)
|
|
10
|
+
|
|
11
|
+
return condition ? options.fn(this) : options.inverse(this)
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function ifEquals(...args) {
|
|
4
|
+
if (args.length < 3) {
|
|
5
|
+
throw Error('At least two parameters must be provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
const condition = args.every((item) => item === args[0])
|
|
10
|
+
|
|
11
|
+
return condition ? options.fn(this) : options.inverse(this)
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function ifEqualsSome(...args) {
|
|
4
|
+
if (args.length < 4) {
|
|
5
|
+
throw Error('At least three parameters must be provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
const control = args.shift()
|
|
10
|
+
const condition = args.some((item) => item === control)
|
|
11
|
+
|
|
12
|
+
return condition ? options.fn(this) : options.inverse(this)
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function ifSome(...args) {
|
|
4
|
+
if (args.length < 2) {
|
|
5
|
+
throw Error('At least one parameter must be provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
const condition = args.some(Boolean)
|
|
10
|
+
|
|
11
|
+
return condition ? options.fn(this) : options.inverse(this)
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export default function json(...args) {
|
|
2
|
+
if (args.length !== 2) {
|
|
3
|
+
throw Error('Incorrect number of parameters provided')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// The second parameter will always be an instance of HelperOptions
|
|
7
|
+
const target = args[0]
|
|
8
|
+
|
|
9
|
+
// Do not allow users to output the whole @root context
|
|
10
|
+
// <https://github.com/Financial-Times/n-handlebars/pull/65>
|
|
11
|
+
if (target && target.hasOwnProperty('_locals')) {
|
|
12
|
+
throw Error('For security reasons you may not use the JSON helper to output the entire view context')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return JSON.stringify(target)
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'path'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import ReactDOMServer from 'react-dom/server'
|
|
4
|
+
|
|
5
|
+
export default function renderReactComponent({ hash }) {
|
|
6
|
+
let modulePath
|
|
7
|
+
|
|
8
|
+
if (hash.hasOwnProperty('package')) {
|
|
9
|
+
// `paths` argument provided to ensure package lookup is made
|
|
10
|
+
// within context of consuming app rather than dependency.
|
|
11
|
+
modulePath = require.resolve(hash.package, { paths: [process.cwd()] })
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (hash.hasOwnProperty('localPath')) {
|
|
15
|
+
// localPath is relative to root of app consuming dotcom-server-handlebars.
|
|
16
|
+
modulePath = path.resolve(hash.localPath)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (!modulePath) {
|
|
20
|
+
throw new Error('You must specify a "package" or "localPath" argument to load a module')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const importedModule = require(modulePath)
|
|
24
|
+
|
|
25
|
+
const Component = hash.hasOwnProperty('namedExport')
|
|
26
|
+
? importedModule[hash.namedExport]
|
|
27
|
+
: importedModule.__esModule
|
|
28
|
+
? importedModule.default
|
|
29
|
+
: importedModule
|
|
30
|
+
|
|
31
|
+
const props = { ...this, ...hash }
|
|
32
|
+
|
|
33
|
+
return ReactDOMServer.renderToStaticMarkup(React.createElement(Component, props))
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
import querystring from 'querystring'
|
|
3
|
+
|
|
4
|
+
const host = 'https://www.ft.com/__origami/service/image/v2/images/raw'
|
|
5
|
+
|
|
6
|
+
const defaults = { source: 'next', fit: 'scale-down' }
|
|
7
|
+
|
|
8
|
+
export default function resize(...args) {
|
|
9
|
+
if (args.length !== 2) {
|
|
10
|
+
throw Error('Incorrect number of parameters provided')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const options = args.pop() as HelperOptions
|
|
14
|
+
const image = options.fn(this)
|
|
15
|
+
const width = args[0]
|
|
16
|
+
|
|
17
|
+
const query = querystring.stringify({ width, ...defaults, ...options.hash })
|
|
18
|
+
|
|
19
|
+
return `${host}/${encodeURIComponent(image)}?${query}`
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function slice(...args) {
|
|
4
|
+
if (args.length !== 2) {
|
|
5
|
+
throw Error('Incorrect number of parameters provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
|
|
10
|
+
const offset = parseInt(options.hash.offset, 10) || 0
|
|
11
|
+
const limit = parseInt(options.hash.limit, 10) || 1
|
|
12
|
+
|
|
13
|
+
const slicedItems = Array.from(args[0]).slice(offset, offset + limit)
|
|
14
|
+
|
|
15
|
+
let contents = ''
|
|
16
|
+
|
|
17
|
+
slicedItems.forEach((item) => {
|
|
18
|
+
contents += options.fn(item)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return contents
|
|
22
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function unlessAll(...args) {
|
|
4
|
+
if (args.length < 2) {
|
|
5
|
+
throw Error('At least one parameter must be provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
const condition = args.some(Boolean) === false
|
|
10
|
+
|
|
11
|
+
return condition ? options.fn(this) : options.inverse(this)
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function unlessEquals(...args) {
|
|
4
|
+
if (args.length < 3) {
|
|
5
|
+
throw Error('At least two parameters must be provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
const condition = args.every((item) => item === args[0]) === false
|
|
10
|
+
|
|
11
|
+
return condition ? options.fn(this) : options.inverse(this)
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { HelperOptions } from 'handlebars'
|
|
2
|
+
|
|
3
|
+
export default function unlessSome(...args) {
|
|
4
|
+
if (args.length < 2) {
|
|
5
|
+
throw Error('At least one parameter must be provided')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const options = args.pop() as HelperOptions
|
|
9
|
+
const condition = args.every(Boolean) === false
|
|
10
|
+
|
|
11
|
+
return condition ? options.fn(this) : options.inverse(this)
|
|
12
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { default as array } from './helpers/array'
|
|
2
|
+
export { default as capture } from './helpers/capture'
|
|
3
|
+
export { default as concat } from './helpers/concat'
|
|
4
|
+
export { default as dateformat } from './helpers/dateformat'
|
|
5
|
+
export { default as encode } from './helpers/encode'
|
|
6
|
+
export { default as ifAll } from './helpers/ifAll'
|
|
7
|
+
export { default as ifEquals } from './helpers/ifEquals'
|
|
8
|
+
export { default as ifEqualsSome } from './helpers/ifEqualsSome'
|
|
9
|
+
export { default as ifSome } from './helpers/ifSome'
|
|
10
|
+
export { default as json } from './helpers/json'
|
|
11
|
+
export { default as renderReactComponent } from './helpers/renderReactComponent'
|
|
12
|
+
export { default as resize } from './helpers/resize'
|
|
13
|
+
export { default as slice } from './helpers/slice'
|
|
14
|
+
export { default as unlessAll } from './helpers/unlessAll'
|
|
15
|
+
export { default as unlessEquals } from './helpers/unlessEquals'
|
|
16
|
+
export { default as unlessSome } from './helpers/unlessSome'
|
package/src/index.ts
ADDED
package/src/types.d.ts
ADDED