Skip to content

Commit

Permalink
Refactor extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
paulrobertlloyd committed Dec 10, 2023
1 parent 9ba7e8b commit 68c220e
Show file tree
Hide file tree
Showing 18 changed files with 336 additions and 248 deletions.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,11 @@ npm install @x-govuk/marked-govspeak

## Usage

Import `govspeak` and and add these extensions to marked with `marked.use()`:

```js
import { marked } from "marked";
import govspeak from "@x-govuk/marked-govspeak";

marked.use({ extensions: govspeak });
marked.use(govspeak());
```

When you call `marked`, the generated HTML will include the classes to style the Govspeak Markdown extensions. For example:
Expand Down Expand Up @@ -61,7 +59,12 @@ The class names used also differ, each prefixed with `govspeak-`. Therefore a `g
If you wish to generate class names that match those from the Govspeak Ruby gem, you can pass the `govspeakGemCompatibility` option to marked. For example:

```js
marked.setOptions({ govspeakGemCompatibility: true });
import { marked } from "marked";
import govspeak from "@x-govuk/marked-govspeak";

marked.use(govspeak({
govspeakGemCompatibility: true
}));

marked("%This is a warning callout%");
```
Expand Down
40 changes: 25 additions & 15 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
module.exports = [
require('./lib/extensions/address'),
require('./lib/extensions/button'),
require('./lib/extensions/call-to-action'),
require('./lib/extensions/contact'),
require('./lib/extensions/form-download'),
require('./lib/extensions/example'),
require('./lib/extensions/information'),
require('./lib/extensions/information-callout'),
require('./lib/extensions/place'),
require('./lib/extensions/stat-headline'),
require('./lib/extensions/steps').steps,
require('./lib/extensions/steps').step,
require('./lib/extensions/warning-callout')
]
/**
* Render Govspeak markup.
*
* @param {object} [options] Options for the extension
* @returns {object} A MarkedExtension to be passed to `marked.use()`
*/
module.exports = function (options = {}) {
return {
extensions: [
require('./lib/extensions/address')(options),
require('./lib/extensions/button')(options),
require('./lib/extensions/call-to-action')(options),
require('./lib/extensions/contact')(options),
require('./lib/extensions/form-download')(options),
require('./lib/extensions/example')(options),
require('./lib/extensions/information')(options),
require('./lib/extensions/information-callout')(options),
require('./lib/extensions/place')(options),
require('./lib/extensions/stat-headline')(options),
require('./lib/extensions/steps').steps(options),
require('./lib/extensions/steps').step(options),
require('./lib/extensions/warning-callout')(options)
]
}
}
5 changes: 3 additions & 2 deletions lib/block-tokenizer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/**
* Parse a Govspeak Markdown block
* @param {import('marked').Lexer} lexer Marked lexer
* @param {string} src Source text to be parsed
* @param {string} name Extension name, i.e. `govspeak-example`
* @param {string} open Opening tag, i.e. $E
* @param {string} [close] Closing tag, i.e. $E
* @returns {object} Tokens
*/
module.exports = function (src, name, open, close) {
module.exports = function (lexer, src, name, open, close) {
if (!src.startsWith(`${open}\n`)) {
return
}
Expand All @@ -22,7 +23,7 @@ module.exports = function (src, name, open, close) {
text: src.slice(closeLength, nextIndex).trim(),
tokens: []
}
this.lexer.blockTokens(token.text, token.tokens)
lexer.blockTokens(token.text, token.tokens)
return token
}
}
5 changes: 3 additions & 2 deletions lib/class-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ const gemCompatibleClassNames = require('./class-names').compatible
/**
* Get class name for given component
* @param {string} component - Component name, i.e. 'button'
* @param {object} [options] - Extension options
* @returns {string} Class name, i.e. 'govuk-button'
*/
module.exports = function (component) {
const classNames = this.parser.options.govspeakGemCompatibility
module.exports = function (component, options = {}) {
const classNames = options.govspeakGemCompatibility
? gemCompatibleClassNames
: defaultClassNames

Expand Down
31 changes: 18 additions & 13 deletions lib/extensions/address.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
const blockTokenizer = require('../block-tokenizer')
const classGenerator = require('../class-generator.js')

module.exports = {
name: 'govspeak-address',
level: 'block',
start (src) {
return src.match(/\$A\n(?!\s)/)?.index
},
tokenizer: function (src) {
return blockTokenizer.bind(this)(src, 'govspeak-address', '$A')
},
renderer (token) {
return `<address class="${classGenerator.bind(this)('address')}">
${this.parser.parse(token.tokens)}
</address>`
module.exports = function (options) {
return {
name: 'govspeak-address',

level: 'block',

start (src) {
return src.match(/\$A\n(?!\s)/)?.index
},

tokenizer (src) {
return blockTokenizer(this.lexer, src, 'govspeak-address', '$A')
},

renderer ({ tokens }) {
const className = classGenerator('address', options)
return `<address class="${className}">\n ${this.parser.parse(tokens)}\n</address>`
}
}
}
76 changes: 42 additions & 34 deletions lib/extensions/button.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,51 @@
const classGenerator = require('../class-generator.js')

module.exports = {
name: 'govspeak-button',
level: 'inline',
start (src) {
return src.match(/{button(?!\s)/)?.index
},
tokenizer (src) {
if (!src.startsWith('{button')) {
return
}
module.exports = function (options) {
return {
name: 'govspeak-button',

let isStartButton = false
let openLength = 8
const closeLength = 9
level: 'inline',

if (src.startsWith('{button start}')) {
isStartButton = true
openLength = 14
}
start (src) {
return src.match(/{button(?!\s)/)?.index
},

const nextIndex = src.indexOf('{/button}', 2)
if (nextIndex !== -1) {
const token = {
type: 'govspeak-button',
raw: src.slice(0, nextIndex + closeLength),
text: src.slice(openLength, nextIndex).trim(),
isStartButton,
tokens: []
tokenizer (src) {
if (!src.startsWith('{button')) {
return
}
this.lexer.inlineTokens(token.text, token.tokens)
return token
}
},
renderer (token) {
if (token.isStartButton) {
return `<a class="${classGenerator.bind(this)('button--start')}" href="${token.tokens[0].href}" role="button">${token.tokens[0].text}<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false"><path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"/></svg></a>`
}

return `<a class="${classGenerator.bind(this)('button')}" href="${token.tokens[0].href}" role="button">${token.tokens[0].text}</a>`
let isStartButton = false
let openLength = 8
const closeLength = 9

if (src.startsWith('{button start}')) {
isStartButton = true
openLength = 14
}

const nextIndex = src.indexOf('{/button}', 2)
if (nextIndex !== -1) {
const token = {
type: 'govspeak-button',
raw: src.slice(0, nextIndex + closeLength),
text: src.slice(openLength, nextIndex).trim(),
isStartButton,
tokens: []
}
this.lexer.inlineTokens(token.text, token.tokens)
return token
}
},

renderer ({ isStartButton, tokens }) {
if (isStartButton) {
const className = classGenerator('button--start', options)
return `<a class="${className}" href="${tokens[0].href}" role="button">${tokens[0].text}<svg class="govuk-button__start-icon" xmlns="http://www.w3.org/2000/svg" width="17.5" height="19" viewBox="0 0 33 40" aria-hidden="true" focusable="false"><path fill="currentColor" d="M0 0h13l20 20-20 20H0l20-20z"/></svg></a>`
}

const className = classGenerator('button', options)
return `<a class="${className}" href="${tokens[0].href}" role="button">${tokens[0].text}</a>`
}
}
}
31 changes: 18 additions & 13 deletions lib/extensions/call-to-action.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
const blockTokenizer = require('../block-tokenizer')
const classGenerator = require('../class-generator.js')

module.exports = {
name: 'govspeak-call-to-action',
level: 'block',
start (src) {
return src.match(/\$CTA(?!\s)/)?.index
},
tokenizer: function (src) {
return blockTokenizer.bind(this)(src, 'govspeak-call-to-action', '$CTA')
},
renderer (token) {
return `<div class="${classGenerator.bind(this)('call-to-action')}">
${this.parser.parse(token.tokens)}
</div>`
module.exports = function (options) {
return {
name: 'govspeak-call-to-action',

level: 'block',

start (src) {
return src.match(/\$CTA(?!\s)/)?.index
},

tokenizer (src) {
return blockTokenizer(this.lexer, src, 'govspeak-call-to-action', '$CTA')
},

renderer ({ tokens }) {
const className = classGenerator('call-to-action', options)
return `<div class="${className}">\n ${this.parser.parse(tokens)}\n</div>`
}
}
}
31 changes: 18 additions & 13 deletions lib/extensions/contact.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
const blockTokenizer = require('../block-tokenizer')
const classGenerator = require('../class-generator.js')

module.exports = {
name: 'govspeak-contact',
level: 'block',
start (src) {
return src.match(/\$C\n(?!\s)/)?.index
},
tokenizer: function (src) {
return blockTokenizer.bind(this)(src, 'govspeak-contact', '$C')
},
renderer (token) {
return `<div class="${classGenerator.bind(this)('contact')}">
${this.parser.parse(token.tokens)}
</div>`
module.exports = function (options) {
return {
name: 'govspeak-contact',

level: 'block',

start (src) {
return src.match(/\$C\n(?!\s)/)?.index
},

tokenizer (src) {
return blockTokenizer(this.lexer, src, 'govspeak-contact', '$C')
},

renderer ({ tokens }) {
const className = classGenerator('contact', options)
return `<div class="${className}">\n ${this.parser.parse(tokens)}\n</div>`
}
}
}
31 changes: 18 additions & 13 deletions lib/extensions/example.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
const blockTokenizer = require('../block-tokenizer')
const classGenerator = require('../class-generator.js')

module.exports = {
name: 'govspeak-example',
level: 'block',
start (src) {
return src.match(/\$E\n(?!\s)/)?.index
},
tokenizer: function (src) {
return blockTokenizer.bind(this)(src, 'govspeak-example', '$E')
},
renderer (token) {
return `<div class="${classGenerator.bind(this)('example')}">
${this.parser.parse(token.tokens)}
</div>`
module.exports = function (options) {
return {
name: 'govspeak-example',

level: 'block',

start (src) {
return src.match(/\$E\n(?!\s)/)?.index
},

tokenizer (src) {
return blockTokenizer(this.lexer, src, 'govspeak-example', '$E')
},

renderer ({ tokens }) {
const className = classGenerator('example', options)
return `<div class="${className}">\n ${this.parser.parse(tokens)}\n</div>`
}
}
}
31 changes: 18 additions & 13 deletions lib/extensions/form-download.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
const blockTokenizer = require('../block-tokenizer')
const classGenerator = require('../class-generator.js')

module.exports = {
name: 'govspeak-form-download',
level: 'block',
start (src) {
return src.match(/\$D\n(?!\s)/)?.index
},
tokenizer: function (src) {
return blockTokenizer.bind(this)(src, 'govspeak-form-download', '$D')
},
renderer (token) {
return `<div class="${classGenerator.bind(this)('form-download')}">
${this.parser.parse(token.tokens)}
</div>`
module.exports = function (options) {
return {
name: 'govspeak-form-download',

level: 'block',

start (src) {
return src.match(/\$D\n(?!\s)/)?.index
},

tokenizer (src) {
return blockTokenizer(this.lexer, src, 'govspeak-form-download', '$D')
},

renderer ({ tokens }) {
const className = classGenerator('form-download', options)
return `<div class="${className}">\n ${this.parser.parse(tokens)}\n</div>`
}
}
}
Loading

0 comments on commit 68c220e

Please sign in to comment.