Skip to content

Commit

Permalink
Merge pull request #667 from alphagov/allow-recursion-in-macro-options
Browse files Browse the repository at this point in the history
Allow recursion in macro options
  • Loading branch information
NickColley authored Dec 18, 2018
2 parents 9f1076c + 75bb992 commit 81f13f9
Show file tree
Hide file tree
Showing 6 changed files with 383 additions and 116 deletions.
113 changes: 0 additions & 113 deletions lib/file-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ const matter = require('gray-matter')

const beautify = require('js-beautify').html

const markdownRenderer = require('marked')

nunjucks.configure(paths.layouts)

// This helper function takes a path of a file and
Expand Down Expand Up @@ -47,117 +45,6 @@ exports.getNunjucksCode = path => {
return content
}

// This helper function takes a path of a macro options file and
// returns the options data grouped by tables to output in markup
exports.getMacroOptions = (componentName, exampleId) => {
let options = []
let processedOptions = []
let primaryTable = {
'name': 'Primary options',
'id': 'primary',
'options': []
}
processedOptions.push(primaryTable)

if (componentName === 'text-input') {
componentName = 'input'
}

try {
let optionsFilePath = path.join(paths.govukfrontendcomponents, componentName, 'macro-options.json')
options = JSON.parse(fs.readFileSync(optionsFilePath, 'utf8'))
} catch (err) {
console.error(err)
process.exit(1) // Exit with a failure mode
}

for (let option of options) {
// Example of an option
// {
// name: "errorMessage",
// type: "string",
// required: false,
// description: "Options for the errorMessage component"
// isComponent: true
// }
if (option.isComponent) {
// Create separate table data for components that are hidden in the
// Design System
if (option.name === 'hint' || option.name === 'label') {
let otherComponentOptions
try {
let otherComponentPath = path.join(paths.govukfrontendcomponents, option.name, 'macro-options.json')
otherComponentOptions = JSON.parse(fs.readFileSync(otherComponentPath, 'utf8'))
} catch (err) {
console.error(err)
process.exit(1) // Exit with a failure mode
}

if (otherComponentOptions) {
processedOptions.push({
'name': 'Options for ' + option.name,
'id': option.name,
'options': otherComponentOptions
})
option.description += ` See [${option.name}](#options-${exampleId}--${option.name}).`
}
// Otherwise just link to that component
} else {
let optionName = (option.name).replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() // camelCase into kebab-case
let otherComponentPath = '/components/' + optionName + '/#options-example-default'
option.description += ` See [${option.name}](${otherComponentPath}).`
}
}

// If option contains nested options, add those as a separate table and link to it
if (option.params) {
processedOptions.push({
'name': 'Options for ' + option.name,
'id': option.name,
'options': option.params
})
option.description += ` See [${option.name}](#options-${exampleId}--${option.name}).`
}

if (option.required === true) {
option.description = '__Required__. ' + option.description
}

primaryTable.options.push(option)
}

// Get reference to marked.js
let renderer = new markdownRenderer.Renderer()
// Override marking up paragraphs
renderer.paragraph = text => text

// This will recursively loop through the options data and call itself when
// encountering a nested item. Any 'description' fields are marked up using
// marked.js
let markUpDescriptions = (options) => {
for (var key in options) {
if (options.hasOwnProperty(key)) {
if (typeof options[key] === 'object') {
markUpDescriptions(options[key])
} else if (key === 'description') {
try {
options[key] = markdownRenderer(options[key], { renderer: (renderer) })
} catch (e) {
console.error(e)
process.exit(1) // Exit with a failure mode
}
}
}
}
return options
}

// Mark up 'description' values in options
let markedUpOptions = processedOptions.map(markUpDescriptions)

return markedUpOptions
}

// This helper function takes a path of a *.md.njk file and
// returns the frontmatter as an object
exports.getFrontmatter = path => {
Expand Down
17 changes: 17 additions & 0 deletions lib/get-macro-options/__mocks__/fs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-env jest */

const fs = jest.genMockFromModule('fs')

let mockedData

fs.__setMockData = data => {
mockedData = data
}
fs.readFileSync = (filename) => {
if (mockedData[filename]) {
return JSON.stringify(mockedData[filename])
}
return '[]'
}

module.exports = fs
153 changes: 153 additions & 0 deletions lib/get-macro-options/__tests__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/* eslint-env jest */

jest.mock('fs')

const fs = require('fs')

const getMacroOptions = require('../index.js')

const fixtures = {
'node_modules/govuk-frontend/components/radios/macro-options.json': [
{
'description': 'Options for a component that exists in the Design System.',
'isComponent': true,
'name': 'componentWithCamelCaseName',
'required': false,
'type': 'object'
},
{
'description': 'Options for a component that does not exist in the Design System (hint component).',
'isComponent': true,
'name': 'hint',
'required': false,
'type': 'object'
},
{
'description': 'Options for a component that does not exist in the Design System (label component).',
'isComponent': true,
'name': 'label',
'required': false,
'type': 'object'
},
{
'description': 'Top level option',
'name': 'classes',
'required': false,
'type': 'string'
},
{
'description': 'Options for the form-group wrapper',
'name': 'formGroup',
'params': [
{
'description': 'Optional classes to add to the form group (e.g. to show error state forthe whole group)',
'name': 'classes',
'required': false,
'type': 'string'
}
]
},
{
'description': 'Options for multiple nested options',
'name': 'nested',
'params': [
{
'description': 'items for nested [link](href)',
'name': 'items',
'required': false,
'params': [
{
'description': '[link](href)',
'name': 'nested-1',
'required': false
},
{
'description': 'Options for a nested component that does not exist in the Design System (label component).',
'isComponent': true,
'name': 'label',
'required': false,
'type': 'object'
},
{
'description': 'Options for a nested component that exists in the Design System.',
'isComponent': true,
'name': 'nestedComponentWithCamelCaseName',
'required': false,
'type': 'object'
}
]
}
]
}
],
'node_modules/govuk-frontend/components/input/macro-options.json': [
{
'description': 'Option for a input component',
'isComponent': true,
'name': 'optionForInputComponent',
'required': false,
'type': 'object'
}
]
}

describe('getMacroOptions', () => {
let output
beforeAll(() => {
fs.__setMockData(fixtures)

output = getMacroOptions('radios')
})
it('has primary options at the top level', () => {
expect(output[0].id).toBe('primary')
expect(output[0].name).toBe('Primary options')
})
describe('slugs', () => {
it('appends slugs to options', () => {
expect(output[0].options[0].slug).toBe('component-with-camel-case-name')
})
it('appends slugs to nested options', () => {
expect(output[2].options[0].params[2].slug).toBe('nested-component-with-camel-case-name')
})
})
describe('nested options', () => {
it('outputs nested options as a separate group', () => {
expect(output[1].id).toBe('formGroup')
})
it('outputs multiple nesting groups', () => {
expect(output[2].options[0].params[0].name).toBe('nested-1')
})
})
describe('additional components', () => {
it('appends additional components that are not in the Design System', () => {
expect(output[4].id).toBe('hint')
expect(output[5].id).toBe('label')
})
it('should only output additional components once', () => {
const optionsWithLabelInName =
Object.entries(output)
.map(entry => {
const name = entry[1].name
return name
})
.filter(name => {
return name.endsWith('label')
})

expect(optionsWithLabelInName.length).toBe(1)
})
})
it('gets input component when requested text-input', () => {
const inputOutput = getMacroOptions('input')

expect(inputOutput[0].options[0].name).toBe('optionForInputComponent')
})
describe('markdown rendering', () => {
it('renders descriptions as markdown', () => {
expect(output[2].options[0].description).toBe('items for nested <a href="href">link</a>')
})
it('renders nested options descriptions as markdown', () => {
expect(output[2].options[0].params[0].description).toBe('<a href="href">link</a>')
})
})
})
Loading

0 comments on commit 81f13f9

Please sign in to comment.