├── src ├── id │ ├── index.js │ ├── id.js │ └── id.test.js ├── html │ ├── index.js │ ├── fixtures │ │ ├── html.txt │ │ ├── empty-array.txt │ │ ├── newline-conversion.txt │ │ ├── empty-array-multiline.txt │ │ ├── newline-conversion-after-newline.txt │ │ ├── nesting-improper.txt │ │ ├── nesting.txt │ │ └── nesting-no-excess.txt │ ├── html.js │ └── html.test.js ├── codeBlock │ └── index.js ├── oneLine │ ├── index.js │ ├── fixtures │ │ ├── oneLine-sentence.txt │ │ └── oneLine.txt │ ├── oneLine.js │ └── oneLine.test.js ├── source │ └── index.js ├── utils │ ├── flat │ │ ├── index.js │ │ ├── flat.js │ │ └── flat.test.js │ ├── toString │ ├── prefixLines │ │ ├── index.js │ │ ├── prefixLines.js │ │ └── prefixLines.test.js │ ├── stripLastNewLine │ │ ├── index.js │ │ ├── stripLastNewLine.js │ │ └── stripLastNewLine.test.js │ ├── index.js │ └── index.test.js ├── commaLists │ ├── index.js │ ├── fixtures │ │ └── commaLists.txt │ ├── commaLists.js │ └── commaLists.test.js ├── createTag │ ├── index.js │ ├── createTag.js │ └── createTag.test.js ├── safeHtml │ ├── index.js │ ├── fixtures │ │ ├── normal-html.txt │ │ ├── newline-conversion.txt │ │ └── escaped-html.txt │ ├── safeHtml.js │ └── safeHtml.test.js ├── TemplateTag │ ├── index.js │ ├── TemplateTag.js │ └── TemplateTag.test.js ├── commaListsOr │ ├── index.js │ ├── fixtures │ │ ├── commaListsOrSingleItem.txt │ │ └── commaListsOr.txt │ ├── commaListsOr.js │ └── commaListsOr.test.js ├── inlineLists │ ├── index.js │ ├── fixtures │ │ └── inlineLists.txt │ ├── inlineLists.js │ └── inlineLists.test.js ├── oneLineTrim │ ├── index.js │ ├── fixtures │ │ └── oneLineTrim.txt │ ├── oneLineTrim.js │ └── oneLineTrim.test.js ├── stripIndent │ ├── index.js │ ├── fixtures │ │ ├── stripIndent.txt │ │ ├── maintainIndent.txt │ │ └── maintainEmptyLines.txt │ ├── stripIndent.js │ └── stripIndent.test.js ├── stripIndents │ ├── index.js │ ├── fixtures │ │ ├── stripIndents.txt │ │ └── maintainEmptyLines.txt │ ├── stripIndents.js │ └── stripIndents.test.js ├── commaListsAnd │ ├── index.js │ ├── fixtures │ │ ├── commaListsAndSingleItem.txt │ │ └── commaListsAnd.txt │ ├── commaListsAnd.js │ └── commaListsAnd.test.js ├── testUtils │ ├── readFromFixture │ │ ├── fixtures │ │ │ └── contents.txt │ │ ├── index.js │ │ ├── readFromFixture.test.js │ │ └── readFromFixture.js │ ├── index.js │ └── index.test.js ├── oneLineCommaLists │ ├── index.js │ ├── fixtures │ │ └── oneLineCommaLists.txt │ ├── oneLineCommaLists.js │ └── oneLineCommaLists.test.js ├── oneLineCommaListsAnd │ ├── index.js │ ├── fixtures │ │ ├── oneLineCommaListsAndSingleItem.txt │ │ └── oneLineCommaListsAnd.txt │ ├── oneLineCommaListsAnd.js │ └── oneLineCommaListsAnd.test.js ├── oneLineCommaListsOr │ ├── index.js │ ├── fixtures │ │ ├── oneLineCommaListsOrSingleItem.txt │ │ └── oneLineCommaListsOr.txt │ ├── oneLineCommaListsOr.js │ └── oneLineCommaListsOr.test.js ├── oneLineInlineLists │ ├── index.js │ ├── fixtures │ │ └── oneLineInlineLists.txt │ ├── oneLineInlineLists.js │ └── oneLineInlineLists.test.js ├── inlineArrayTransformer │ ├── index.js │ ├── inlineArrayTransformer.js │ └── inlineArrayTransformer.test.js ├── splitStringTransformer │ ├── index.js │ ├── splitStringTransformer.js │ └── splitStringTransformer.test.js ├── stripIndentTransformer │ ├── index.js │ ├── fixtures │ │ ├── stripIndents.txt │ │ └── stripIndent.txt │ ├── stripIndentTransformer.js │ └── stripIndentTransformer.test.js ├── trimResultTransformer │ ├── index.js │ ├── trimResultTransformer.js │ └── trimResultTransformer.test.js ├── replaceResultTransformer │ ├── index.js │ ├── replaceResultTransformer.js │ └── replaceResultTransformer.test.js ├── replaceStringTransformer │ ├── index.js │ ├── replaceStringTransformer.js │ └── replaceStringTransformer.test.js ├── replaceSubstitutionTransformer │ ├── index.js │ ├── replaceSubstitutionTransformer.js │ └── replaceSubstitutionTransformer.test.js ├── removeNonPrintingValuesTransformer │ ├── index.js │ ├── removeNonPrintingValuesTransformer.js │ └── removeNonPrintingValuesTransformer.test.js ├── index.test.js └── index.js ├── .prettierrc ├── jest.config.js ├── .npmignore ├── .travis.yml ├── .editorconfig ├── rollup.config.js ├── appveyor.yml ├── contributing.md ├── .gitignore ├── .babelrc.js ├── .eslintrc.js ├── license.md ├── package.json ├── media └── logo.svg └── readme.md /src/id/index.js: -------------------------------------------------------------------------------- 1 | export default from './id'; 2 | -------------------------------------------------------------------------------- /src/html/index.js: -------------------------------------------------------------------------------- 1 | export default from './html'; 2 | -------------------------------------------------------------------------------- /src/codeBlock/index.js: -------------------------------------------------------------------------------- 1 | export default from '../html'; 2 | -------------------------------------------------------------------------------- /src/oneLine/index.js: -------------------------------------------------------------------------------- 1 | export default from './oneLine'; 2 | -------------------------------------------------------------------------------- /src/source/index.js: -------------------------------------------------------------------------------- 1 | export default from '../html'; 2 | -------------------------------------------------------------------------------- /src/utils/flat/index.js: -------------------------------------------------------------------------------- 1 | export default from './flat'; 2 | -------------------------------------------------------------------------------- /src/commaLists/index.js: -------------------------------------------------------------------------------- 1 | export default from './commaLists'; 2 | -------------------------------------------------------------------------------- /src/createTag/index.js: -------------------------------------------------------------------------------- 1 | export default from './createTag'; 2 | -------------------------------------------------------------------------------- /src/safeHtml/index.js: -------------------------------------------------------------------------------- 1 | export default from './safeHtml'; 2 | -------------------------------------------------------------------------------- /src/TemplateTag/index.js: -------------------------------------------------------------------------------- 1 | export default from './TemplateTag'; 2 | -------------------------------------------------------------------------------- /src/commaListsOr/index.js: -------------------------------------------------------------------------------- 1 | export default from './commaListsOr'; 2 | -------------------------------------------------------------------------------- /src/inlineLists/index.js: -------------------------------------------------------------------------------- 1 | export default from './inlineLists'; 2 | -------------------------------------------------------------------------------- /src/oneLineTrim/index.js: -------------------------------------------------------------------------------- 1 | export default from './oneLineTrim'; 2 | -------------------------------------------------------------------------------- /src/stripIndent/index.js: -------------------------------------------------------------------------------- 1 | export default from './stripIndent'; 2 | -------------------------------------------------------------------------------- /src/stripIndents/index.js: -------------------------------------------------------------------------------- 1 | export default from './stripIndents'; 2 | -------------------------------------------------------------------------------- /src/utils/toString/index.js: -------------------------------------------------------------------------------- 1 | export default from './toString'; 2 | -------------------------------------------------------------------------------- /src/commaListsAnd/index.js: -------------------------------------------------------------------------------- 1 | export default from './commaListsAnd'; 2 | -------------------------------------------------------------------------------- /src/testUtils/readFromFixture/fixtures/contents.txt: -------------------------------------------------------------------------------- 1 | wow such doge 2 | -------------------------------------------------------------------------------- /src/utils/prefixLines/index.js: -------------------------------------------------------------------------------- 1 | export default from './prefixLines'; 2 | -------------------------------------------------------------------------------- /src/oneLineCommaLists/index.js: -------------------------------------------------------------------------------- 1 | export default from './oneLineCommaLists'; 2 | -------------------------------------------------------------------------------- /src/testUtils/index.js: -------------------------------------------------------------------------------- 1 | export readFromFixture from './readFromFixture'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /src/oneLineCommaListsAnd/index.js: -------------------------------------------------------------------------------- 1 | export default from './oneLineCommaListsAnd'; 2 | -------------------------------------------------------------------------------- /src/oneLineCommaListsOr/index.js: -------------------------------------------------------------------------------- 1 | export default from './oneLineCommaListsOr'; 2 | -------------------------------------------------------------------------------- /src/oneLineInlineLists/index.js: -------------------------------------------------------------------------------- 1 | export default from './oneLineInlineLists'; 2 | -------------------------------------------------------------------------------- /src/stripIndent/fixtures/stripIndent.txt: -------------------------------------------------------------------------------- 1 | wow such indent gone 2 | very amaze 3 | -------------------------------------------------------------------------------- /src/testUtils/readFromFixture/index.js: -------------------------------------------------------------------------------- 1 | export default from './readFromFixture'; 2 | -------------------------------------------------------------------------------- /src/utils/stripLastNewLine/index.js: -------------------------------------------------------------------------------- 1 | export default from './stripLastNewLine'; 2 | -------------------------------------------------------------------------------- /src/inlineArrayTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './inlineArrayTransformer'; 2 | -------------------------------------------------------------------------------- /src/splitStringTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './splitStringTransformer'; 2 | -------------------------------------------------------------------------------- /src/stripIndent/fixtures/maintainIndent.txt: -------------------------------------------------------------------------------- 1 | wow such indent gone 2 | very amaze 3 | -------------------------------------------------------------------------------- /src/stripIndentTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './stripIndentTransformer'; 2 | -------------------------------------------------------------------------------- /src/trimResultTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './trimResultTransformer'; 2 | -------------------------------------------------------------------------------- /src/replaceResultTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './replaceResultTransformer'; 2 | -------------------------------------------------------------------------------- /src/replaceStringTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './replaceStringTransformer'; 2 | -------------------------------------------------------------------------------- /src/oneLine/fixtures/oneLine-sentence.txt: -------------------------------------------------------------------------------- 1 | Sentences also work. Double spacing is preserved. 2 | -------------------------------------------------------------------------------- /src/oneLine/fixtures/oneLine.txt: -------------------------------------------------------------------------------- 1 | wow such doge is very amaze at one line neat from multiline 2 | -------------------------------------------------------------------------------- /src/oneLineTrim/fixtures/oneLineTrim.txt: -------------------------------------------------------------------------------- 1 | wow such reductionvery absence of spacemuch amaze 2 | -------------------------------------------------------------------------------- /src/stripIndent/fixtures/maintainEmptyLines.txt: -------------------------------------------------------------------------------- 1 | wow such indent gone 2 | 3 | very amaze 4 | -------------------------------------------------------------------------------- /src/stripIndents/fixtures/stripIndents.txt: -------------------------------------------------------------------------------- 1 | wow such indent gone 2 | very amaze 3 | foo bar baz 4 | -------------------------------------------------------------------------------- /src/replaceSubstitutionTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './replaceSubstitutionTransformer'; 2 | -------------------------------------------------------------------------------- /src/commaLists/fixtures/commaLists.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple, banana, kiwi 2 | they are amaze 3 | -------------------------------------------------------------------------------- /src/commaListsAnd/fixtures/commaListsAndSingleItem.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple 2 | they are amaze 3 | -------------------------------------------------------------------------------- /src/commaListsOr/fixtures/commaListsOrSingleItem.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple 2 | they are amaze 3 | -------------------------------------------------------------------------------- /src/inlineLists/fixtures/inlineLists.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple banana kiwi 2 | they are amaze 3 | -------------------------------------------------------------------------------- /src/utils/flat/flat.js: -------------------------------------------------------------------------------- 1 | export default function flat(array) { 2 | return [].concat(...array); 3 | } 4 | -------------------------------------------------------------------------------- /src/commaListsOr/fixtures/commaListsOr.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple, banana or kiwi 2 | they are amaze 3 | -------------------------------------------------------------------------------- /src/removeNonPrintingValuesTransformer/index.js: -------------------------------------------------------------------------------- 1 | export default from './removeNonPrintingValuesTransformer'; 2 | -------------------------------------------------------------------------------- /src/safeHtml/fixtures/normal-html.txt: -------------------------------------------------------------------------------- 1 |

amaze

2 | 7 | -------------------------------------------------------------------------------- /src/stripIndents/fixtures/maintainEmptyLines.txt: -------------------------------------------------------------------------------- 1 | wow such indent gone 2 | very amaze 3 | 4 | foo bar baz 5 | -------------------------------------------------------------------------------- /src/commaListsAnd/fixtures/commaListsAnd.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple, banana and kiwi 2 | they are amaze 3 | -------------------------------------------------------------------------------- /src/id/id.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | 3 | const id = createTag(); 4 | 5 | export default id; 6 | -------------------------------------------------------------------------------- /src/oneLineCommaLists/fixtures/oneLineCommaLists.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple, banana, kiwi they are amaze 2 | -------------------------------------------------------------------------------- /src/oneLineCommaListsAnd/fixtures/oneLineCommaListsAndSingleItem.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple they are amaze 2 | -------------------------------------------------------------------------------- /src/oneLineCommaListsOr/fixtures/oneLineCommaListsOrSingleItem.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple they are amaze 2 | -------------------------------------------------------------------------------- /src/oneLineInlineLists/fixtures/oneLineInlineLists.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple banana kiwi they are amaze 2 | -------------------------------------------------------------------------------- /src/stripIndentTransformer/fixtures/stripIndents.txt: -------------------------------------------------------------------------------- 1 | foo bar baz 2 | bar baz foo 3 | baz foo bar 4 | wow such doge 5 | -------------------------------------------------------------------------------- /src/oneLineCommaListsOr/fixtures/oneLineCommaListsOr.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple, banana or kiwi they are amaze 2 | -------------------------------------------------------------------------------- /src/safeHtml/fixtures/newline-conversion.txt: -------------------------------------------------------------------------------- 1 |

amaze

2 | 7 | -------------------------------------------------------------------------------- /src/stripIndentTransformer/fixtures/stripIndent.txt: -------------------------------------------------------------------------------- 1 | foo bar baz 2 | bar baz foo 3 | baz foo bar 4 | wow such doge 5 | -------------------------------------------------------------------------------- /src/html/fixtures/html.txt: -------------------------------------------------------------------------------- 1 |

amaze

2 | 7 | -------------------------------------------------------------------------------- /src/oneLineCommaListsAnd/fixtures/oneLineCommaListsAnd.txt: -------------------------------------------------------------------------------- 1 | Doge <3's these fruits: apple, banana and kiwi they are amaze 2 | -------------------------------------------------------------------------------- /src/html/fixtures/empty-array.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/html/fixtures/newline-conversion.txt: -------------------------------------------------------------------------------- 1 |

amaze

2 | 7 | -------------------------------------------------------------------------------- /src/html/fixtures/empty-array-multiline.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/html/fixtures/newline-conversion-after-newline.txt: -------------------------------------------------------------------------------- 1 |

amaze

2 | 8 | -------------------------------------------------------------------------------- /src/safeHtml/fixtures/escaped-html.txt: -------------------------------------------------------------------------------- 1 |

amaze

2 | 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | collectCoverage: true, 5 | coverageDirectory: './coverage/', 6 | testEnvironment: 'node', 7 | }; 8 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export flat from './flat'; 2 | export prefixLines from './prefixLines'; 3 | export stripLastNewLine from './stripLastNewLine'; 4 | export toString from './toString'; 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | .babelrc 3 | .babelrc.js 4 | .editorconfig 5 | .gitignore 6 | .travis.yml 7 | .appveyor.yml 8 | contributing.md 9 | .nyc_output 10 | coverage 11 | lib/**/*.test.js 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | cache: 3 | directories: 4 | - node_modules 5 | notifications: 6 | email: true 7 | node_js: 8 | - 14 9 | - 13 10 | - 12 11 | - 10 12 | script: 13 | - npm test 14 | after_script: 15 | - npm run codecov 16 | -------------------------------------------------------------------------------- /src/id/id.test.js: -------------------------------------------------------------------------------- 1 | import id from './id'; 2 | 3 | test('returns whatever comes at it', () => { 4 | expect(id`foo${42}bar`).toBe('foo42bar'); 5 | }); 6 | 7 | test('returns whatever comes at it (number version)', () => { 8 | expect(id(42)).toBe(42); 9 | }); 10 | -------------------------------------------------------------------------------- /src/html/fixtures/nesting-improper.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/inlineLists/inlineLists.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndent from '../stripIndent'; 3 | import inlineArrayTransformer from '../inlineArrayTransformer'; 4 | 5 | const inlineLists = createTag(inlineArrayTransformer(), stripIndent); 6 | 7 | export default inlineLists; 8 | -------------------------------------------------------------------------------- /src/utils/toString/toString.js: -------------------------------------------------------------------------------- 1 | export default function toString(value) { 2 | // Use concat rather than string so that the behavior is the same as when 3 | // expressions are evaluated in templates (look for Runtime Semantics: 4 | // Evaluation of a TemplateLiteral). 5 | return ''.concat(value); 6 | } 7 | -------------------------------------------------------------------------------- /src/html/fixtures/nesting.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/commaLists/commaLists.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndent from '../stripIndent'; 3 | import inlineArrayTransformer from '../inlineArrayTransformer'; 4 | 5 | const commaLists = createTag( 6 | inlineArrayTransformer({ separator: ',' }), 7 | stripIndent, 8 | ); 9 | 10 | export default commaLists; 11 | -------------------------------------------------------------------------------- /src/html/fixtures/nesting-no-excess.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/utils/prefixLines/prefixLines.js: -------------------------------------------------------------------------------- 1 | import toString from '../toString'; 2 | 3 | export default function prefixLines(prefix, value, skipFirst = false) { 4 | return toString(value) 5 | .split('\n') 6 | .map((line, index) => 7 | skipFirst && index === 0 ? line : ''.concat(prefix, line), 8 | ) 9 | .join('\n'); 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/stripLastNewLine/stripLastNewLine.js: -------------------------------------------------------------------------------- 1 | import toString from '../toString'; 2 | 3 | export default function stripLastNewLine(value) { 4 | const stringValue = toString(value); 5 | const { length } = stringValue; 6 | return length > 0 && stringValue[length - 1] === '\n' 7 | ? stringValue.slice(0, length - 1) 8 | : stringValue; 9 | } 10 | -------------------------------------------------------------------------------- /src/commaListsOr/commaListsOr.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndent from '../stripIndent'; 3 | import inlineArrayTransformer from '../inlineArrayTransformer'; 4 | 5 | const commaListsOr = createTag( 6 | inlineArrayTransformer({ separator: ',', conjunction: 'or' }), 7 | stripIndent, 8 | ); 9 | 10 | export default commaListsOr; 11 | -------------------------------------------------------------------------------- /src/stripIndent/stripIndent.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndentTransformer from '../stripIndentTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | 5 | const stripIndent = createTag( 6 | stripIndentTransformer(), 7 | trimResultTransformer('smart'), 8 | ); 9 | 10 | export default stripIndent; 11 | -------------------------------------------------------------------------------- /src/commaListsAnd/commaListsAnd.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndent from '../stripIndent'; 3 | import inlineArrayTransformer from '../inlineArrayTransformer'; 4 | 5 | const commaListsAnd = createTag( 6 | inlineArrayTransformer({ separator: ',', conjunction: 'and' }), 7 | stripIndent, 8 | ); 9 | 10 | export default commaListsAnd; 11 | -------------------------------------------------------------------------------- /src/oneLine/oneLine.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import trimResultTransformer from '../trimResultTransformer'; 3 | import replaceResultTransformer from '../replaceResultTransformer'; 4 | 5 | const oneLine = createTag( 6 | replaceResultTransformer(/(?:\n(?:\s*))+/g, ' '), 7 | trimResultTransformer(), 8 | ); 9 | 10 | export default oneLine; 11 | -------------------------------------------------------------------------------- /src/stripIndents/stripIndents.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndentTransformer from '../stripIndentTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | 5 | const stripIndents = createTag( 6 | stripIndentTransformer('all'), 7 | trimResultTransformer('smart'), 8 | ); 9 | 10 | export default stripIndents; 11 | -------------------------------------------------------------------------------- /src/oneLineTrim/oneLineTrim.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import trimResultTransformer from '../trimResultTransformer'; 3 | import replaceResultTransformer from '../replaceResultTransformer'; 4 | 5 | const oneLineTrim = createTag( 6 | replaceResultTransformer(/(?:\n\s*)/g, ''), 7 | trimResultTransformer(), 8 | ); 9 | 10 | export default oneLineTrim; 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | indent_size = 2 13 | 14 | [*.md] 15 | indent_size = 4 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from '@rollup/plugin-babel'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import { uglify } from 'rollup-plugin-uglify'; 4 | 5 | export default { 6 | input: 'src/index.js', 7 | output: { 8 | extend: true, 9 | file: 'dist/common-tags.min.js', 10 | format: 'umd', 11 | indent: false, 12 | name: 'commonTags', 13 | }, 14 | plugins: [babel(), resolve(), uglify()], 15 | }; 16 | -------------------------------------------------------------------------------- /src/replaceStringTransformer/replaceStringTransformer.js: -------------------------------------------------------------------------------- 1 | const replaceStringTransformer = (replaceWhat, replaceWith) => { 2 | if (replaceWhat == null || replaceWith == null) { 3 | throw new Error('replaceStringTransformer requires exactly 2 arguments.'); 4 | } 5 | 6 | return { 7 | onString(str) { 8 | return str.replace(replaceWhat, replaceWith); 9 | }, 10 | }; 11 | }; 12 | 13 | export default replaceStringTransformer; 14 | -------------------------------------------------------------------------------- /src/testUtils/readFromFixture/readFromFixture.test.js: -------------------------------------------------------------------------------- 1 | import readFromFixture from './readFromFixture'; 2 | 3 | test('reads the correct fixture contents', () => { 4 | const actual = readFromFixture(__dirname, 'contents'); 5 | const expected = 'wow such doge\n'; 6 | expect(actual).toBe(expected); 7 | }); 8 | 9 | test('should throw if no file was found', () => { 10 | expect(() => { 11 | readFromFixture(__dirname, 'nothing'); 12 | }).toThrow(/ENOENT/); 13 | }); 14 | -------------------------------------------------------------------------------- /src/commaLists/commaLists.test.js: -------------------------------------------------------------------------------- 1 | import commaLists from './commaLists'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as comma-separated list', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'commaLists'); 9 | const actual = commaLists` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | -------------------------------------------------------------------------------- /src/inlineLists/inlineLists.test.js: -------------------------------------------------------------------------------- 1 | import inlineLists from './inlineLists'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as space-separated list', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'inlineLists'); 9 | const actual = inlineLists` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | -------------------------------------------------------------------------------- /src/oneLineInlineLists/oneLineInlineLists.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import inlineArrayTransformer from '../inlineArrayTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | import replaceResultTransformer from '../replaceResultTransformer'; 5 | 6 | const oneLineInlineLists = createTag( 7 | inlineArrayTransformer(), 8 | replaceResultTransformer(/(?:\s+)/g, ' '), 9 | trimResultTransformer(), 10 | ); 11 | 12 | export default oneLineInlineLists; 13 | -------------------------------------------------------------------------------- /src/utils/flat/flat.test.js: -------------------------------------------------------------------------------- 1 | import flat from './flat'; 2 | 3 | test('leaves flat array as-is', () => { 4 | expect(flat([1, 2, 3])).toEqual([1, 2, 3]); 5 | }); 6 | 7 | test('flattens array elements', () => { 8 | expect(flat([[1], [2, 3]])).toEqual([1, 2, 3]); 9 | }); 10 | 11 | test('handles mixed content', () => { 12 | expect(flat([1, [2, 3]])).toEqual([1, 2, 3]); 13 | }); 14 | 15 | test("doesn't flatten more than 1 level", () => { 16 | expect(flat([1, [2, [3]]])).toEqual([1, 2, [3]]); 17 | }); 18 | -------------------------------------------------------------------------------- /src/oneLineCommaLists/oneLineCommaLists.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import inlineArrayTransformer from '../inlineArrayTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | import replaceResultTransformer from '../replaceResultTransformer'; 5 | 6 | const oneLineCommaLists = createTag( 7 | inlineArrayTransformer({ separator: ',' }), 8 | replaceResultTransformer(/(?:\s+)/g, ' '), 9 | trimResultTransformer(), 10 | ); 11 | 12 | export default oneLineCommaLists; 13 | -------------------------------------------------------------------------------- /src/html/html.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndent from '../stripIndent'; 3 | import inlineArrayTransformer from '../inlineArrayTransformer'; 4 | import splitStringTransformer from '../splitStringTransformer'; 5 | import removeNonPrintingValuesTransformer from '../removeNonPrintingValuesTransformer'; 6 | 7 | const html = createTag( 8 | splitStringTransformer('\n'), 9 | removeNonPrintingValuesTransformer(), 10 | inlineArrayTransformer(), 11 | stripIndent, 12 | ); 13 | 14 | export default html; 15 | -------------------------------------------------------------------------------- /src/oneLineCommaListsOr/oneLineCommaListsOr.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import inlineArrayTransformer from '../inlineArrayTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | import replaceResultTransformer from '../replaceResultTransformer'; 5 | 6 | const oneLineCommaListsOr = createTag( 7 | inlineArrayTransformer({ separator: ',', conjunction: 'or' }), 8 | replaceResultTransformer(/(?:\s+)/g, ' '), 9 | trimResultTransformer(), 10 | ); 11 | 12 | export default oneLineCommaListsOr; 13 | -------------------------------------------------------------------------------- /src/splitStringTransformer/splitStringTransformer.js: -------------------------------------------------------------------------------- 1 | const splitStringTransformer = (splitBy) => { 2 | if (typeof splitBy !== 'string') { 3 | throw new Error('You need to specify a string character to split by.'); 4 | } 5 | 6 | return { 7 | onSubstitution(substitution) { 8 | if (typeof substitution === 'string' && substitution.includes(splitBy)) { 9 | return substitution.split(splitBy); 10 | } 11 | return substitution; 12 | }, 13 | }; 14 | }; 15 | 16 | export default splitStringTransformer; 17 | -------------------------------------------------------------------------------- /src/oneLineCommaListsAnd/oneLineCommaListsAnd.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import inlineArrayTransformer from '../inlineArrayTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | import replaceResultTransformer from '../replaceResultTransformer'; 5 | 6 | const oneLineCommaListsAnd = createTag( 7 | inlineArrayTransformer({ separator: ',', conjunction: 'and' }), 8 | replaceResultTransformer(/(?:\s+)/g, ' '), 9 | trimResultTransformer(), 10 | ); 11 | 12 | export default oneLineCommaListsAnd; 13 | -------------------------------------------------------------------------------- /src/oneLineCommaLists/oneLineCommaLists.test.js: -------------------------------------------------------------------------------- 1 | import oneLineCommaLists from './oneLineCommaLists'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as comma-separated list on one line', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'oneLineCommaLists').trim(); 9 | const actual = oneLineCommaLists` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | -------------------------------------------------------------------------------- /src/oneLineInlineLists/oneLineInlineLists.test.js: -------------------------------------------------------------------------------- 1 | import oneLineInlineLists from './oneLineInlineLists'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as inline list on one line', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'oneLineInlineLists').trim(); 9 | const actual = oneLineInlineLists` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | -------------------------------------------------------------------------------- /src/removeNonPrintingValuesTransformer/removeNonPrintingValuesTransformer.js: -------------------------------------------------------------------------------- 1 | const isValidValue = (x) => 2 | x != null && !Number.isNaN(x) && typeof x !== 'boolean'; 3 | 4 | const removeNonPrintingValuesTransformer = () => ({ 5 | onSubstitution(substitution) { 6 | if (Array.isArray(substitution)) { 7 | return substitution.filter(isValidValue); 8 | } 9 | if (isValidValue(substitution)) { 10 | return substitution; 11 | } 12 | return ''; 13 | }, 14 | }); 15 | 16 | export default removeNonPrintingValuesTransformer; 17 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: 14 4 | - nodejs_version: 13 5 | - nodejs_version: 12 6 | - nodejs_version: 10 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - set CI=true 10 | - npm install --global npm@latest 11 | - set PATH=%APPDATA%\npm;%PATH% 12 | - npm install 13 | matrix: 14 | fast_finish: true 15 | build: off 16 | test_script: 17 | - node --version 18 | - npm --version 19 | - npm test 20 | after_test: 21 | - npm run codecov 22 | cache: 23 | - '%APPDATA%\npm-cache' 24 | -------------------------------------------------------------------------------- /src/testUtils/readFromFixture/readFromFixture.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | /** 5 | * reads the text contents of .txt in the fixtures folder 6 | * relative to the caller module's test file 7 | * @param {String} name - the name of the fixture you want to read 8 | * @return {Promise} - the retrieved fixture's file contents 9 | */ 10 | export default function readFromFixture(dirname, name) { 11 | const contents = fs.readFileSync( 12 | path.join(dirname, `fixtures/${name}.txt`), 13 | 'utf8', 14 | ); 15 | return contents.replace(/\r\n/g, '\n'); 16 | } 17 | -------------------------------------------------------------------------------- /src/replaceStringTransformer/replaceStringTransformer.test.js: -------------------------------------------------------------------------------- 1 | import replaceStringTransformer from './replaceStringTransformer'; 2 | import createTag from '../createTag'; 3 | 4 | test('only operates on strings', () => { 5 | const tag = createTag( 6 | replaceStringTransformer(//g, '>'), 8 | ); 9 | expect(tag`

foo${''}

`).toBe( 10 | '<h1>foo</h1>', 11 | ); 12 | }); 13 | 14 | test('throws error if no arguments are supplied', () => { 15 | expect(() => { 16 | replaceStringTransformer(); 17 | }).toThrow(/requires exactly 2 arguments/); 18 | }); 19 | -------------------------------------------------------------------------------- /src/replaceSubstitutionTransformer/replaceSubstitutionTransformer.js: -------------------------------------------------------------------------------- 1 | const replaceSubstitutionTransformer = (replaceWhat, replaceWith) => { 2 | if (replaceWhat == null || replaceWith == null) { 3 | throw new Error( 4 | 'replaceSubstitutionTransformer requires exactly 2 arguments.', 5 | ); 6 | } 7 | 8 | return { 9 | onSubstitution(substitution) { 10 | // Do not touch if null or undefined 11 | if (substitution == null) { 12 | return substitution; 13 | } else { 14 | return String(substitution).replace(replaceWhat, replaceWith); 15 | } 16 | }, 17 | }; 18 | }; 19 | 20 | export default replaceSubstitutionTransformer; 21 | -------------------------------------------------------------------------------- /src/utils/toString/toString.test.js: -------------------------------------------------------------------------------- 1 | import toString from './toString'; 2 | 3 | test('transforms values to string as per spec', () => { 4 | const get = jest 5 | .fn() 6 | .mockImplementationOnce((target, prop) => { 7 | expect(prop).toBe(Symbol.toPrimitive); 8 | }) 9 | .mockImplementationOnce((target, prop) => { 10 | expect(prop).toBe('toString'); 11 | }) 12 | .mockImplementationOnce((target, prop) => { 13 | expect(prop).toBe('valueOf'); 14 | return () => 42; 15 | }); 16 | 17 | const val = new Proxy({}, { get }); 18 | const result = toString(val); 19 | 20 | expect(get).toHaveBeenCalledTimes(3); 21 | expect(result).toBe('42'); 22 | }); 23 | -------------------------------------------------------------------------------- /src/stripIndents/stripIndents.test.js: -------------------------------------------------------------------------------- 1 | import stripIndents from './stripIndents'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('strips all indentation', () => { 7 | const expected = readFromFixture(__dirname, 'stripIndents'); 8 | const actual = stripIndents` 9 | wow such indent gone 10 | very ${val} 11 | foo bar baz 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | 16 | test('maintains empty lines', () => { 17 | const expected = readFromFixture(__dirname, 'maintainEmptyLines'); 18 | const actual = stripIndents` 19 | wow such indent gone 20 | very ${val} 21 | 22 | foo bar baz 23 | `; 24 | expect(actual).toBe(expected); 25 | }); 26 | -------------------------------------------------------------------------------- /src/utils/stripLastNewLine/stripLastNewLine.test.js: -------------------------------------------------------------------------------- 1 | import stripLastNewLine from './stripLastNewLine'; 2 | 3 | test('it should strip last new line with a given string', () => { 4 | const actual = stripLastNewLine('foo\n'); 5 | const expected = 'foo'; 6 | 7 | expect(actual).toEqual(expected); 8 | }); 9 | 10 | test('it should leave a string as is when the last character is not a new line', () => { 11 | const actual = stripLastNewLine('foo'); 12 | const expected = 'foo'; 13 | 14 | expect(actual).toEqual(expected); 15 | }); 16 | 17 | test('it should return an empty string for an empty string', () => { 18 | const actual = stripLastNewLine(''); 19 | const expected = ''; 20 | 21 | expect(actual).toEqual(expected); 22 | }); 23 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | ## Setup 4 | 5 | 1. Fork this repository 6 | 2. `git clone` your fork down to your local machine 7 | 3. `cd` into the directory for your fork 8 | 4. run `npm install` 9 | 10 | ## Tests 11 | 12 | Please include _passing_ tests for any new logic or features you include in 13 | your contribution. This project uses [AVA](/sindresorhus/ava) to run tests and 14 | is setup to look for files that end with `.test.js` in the `src` directory. 15 | 16 | Furthermore, this project uses [Prettier](/prettier/prettier) and 17 | [ESLint](/eslint/eslint) for linting & formatting, and tests will _fail_ if 18 | your code does not adhere to the rules (run `npm lint` or `npm lint:fix` to 19 | check/fix the code). 20 | -------------------------------------------------------------------------------- /src/oneLineTrim/oneLineTrim.test.js: -------------------------------------------------------------------------------- 1 | import oneLineTrim from './oneLineTrim'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('reduces to one line while trimming newlines', () => { 7 | const expected = readFromFixture(__dirname, 'oneLineTrim').trim(); 8 | const actual = oneLineTrim` 9 | wow such reduction 10 | very absence of space 11 | much ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | 16 | test('reduces to one line while trimming newlines (no indentation)', () => { 17 | const expected = readFromFixture(__dirname, 'oneLineTrim').trim(); 18 | const actual = oneLineTrim` 19 | wow such reduction 20 | very absence of space 21 | much ${val} 22 | `; 23 | expect(actual).toBe(expected); 24 | }); 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | 3 | ### Node ### 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | dist 10 | lib 11 | es 12 | .DS_Store 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directory 36 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 37 | node_modules 38 | -------------------------------------------------------------------------------- /src/replaceResultTransformer/replaceResultTransformer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Replaces tabs, newlines and spaces with the chosen value when they occur in sequences 3 | * @param {(String|RegExp)} replaceWhat - the value or pattern that should be replaced 4 | * @param {*} replaceWith - the replacement value 5 | * @return {Object} - a TemplateTag transformer 6 | */ 7 | const replaceResultTransformer = (replaceWhat, replaceWith) => { 8 | if (replaceWhat == null || replaceWith == null) { 9 | throw new Error('replaceResultTransformer requires exactly 2 arguments.'); 10 | } 11 | 12 | return { 13 | onEndResult(endResult) { 14 | return endResult.replace(replaceWhat, replaceWith); 15 | }, 16 | }; 17 | }; 18 | 19 | export default replaceResultTransformer; 20 | -------------------------------------------------------------------------------- /src/safeHtml/safeHtml.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndent from '../stripIndent'; 3 | import inlineArrayTransformer from '../inlineArrayTransformer'; 4 | import splitStringTransformer from '../splitStringTransformer'; 5 | import replaceSubstitutionTransformer from '../replaceSubstitutionTransformer'; 6 | 7 | const safeHtml = createTag( 8 | splitStringTransformer('\n'), 9 | inlineArrayTransformer(), 10 | stripIndent, 11 | replaceSubstitutionTransformer(/&/g, '&'), 12 | replaceSubstitutionTransformer(//g, '>'), 14 | replaceSubstitutionTransformer(/"/g, '"'), 15 | replaceSubstitutionTransformer(/'/g, '''), 16 | replaceSubstitutionTransformer(/`/g, '`'), 17 | ); 18 | 19 | export default safeHtml; 20 | -------------------------------------------------------------------------------- /src/utils/prefixLines/prefixLines.test.js: -------------------------------------------------------------------------------- 1 | import prefixLines from './prefixLines'; 2 | 3 | test('it should prefix lines with a given string', () => { 4 | const actual = prefixLines('foo', ' a \n b \n c '); 5 | const expected = 'foo a \nfoo b \nfoo c '; 6 | 7 | expect(actual).toEqual(expected); 8 | }); 9 | 10 | test('it should prefix lines with a given non-string value', () => { 11 | const actual = prefixLines(42, ' a \n b \n c '); 12 | const expected = '42 a \n42 b \n42 c '; 13 | 14 | expect(actual).toEqual(expected); 15 | }); 16 | 17 | test('it should skip the first line with the option set', () => { 18 | const actual = prefixLines('foo', ' a \n b \n c ', true); 19 | const expected = ' a \nfoo b \nfoo c '; 20 | 21 | expect(actual).toEqual(expected); 22 | }); 23 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const presetEnv = require('@babel/preset-env'); 4 | const pluginProposalClassProperties = require('@babel/plugin-proposal-class-properties'); 5 | const pluginProposalExportDefaultFrom = require('@babel/plugin-proposal-export-default-from'); 6 | const pluginAddModuleExports = require('babel-plugin-add-module-exports'); 7 | 8 | const isEsEnv = process.env.BABEL_ENV === 'es'; 9 | 10 | module.exports = { 11 | sourceMaps: 'inline', 12 | 13 | presets: [ 14 | [ 15 | presetEnv, 16 | { 17 | modules: isEsEnv ? false : 'commonjs', 18 | loose: true, 19 | }, 20 | ], 21 | ], 22 | 23 | plugins: [ 24 | ...(isEsEnv ? [] : [pluginAddModuleExports]), 25 | pluginProposalClassProperties, 26 | pluginProposalExportDefaultFrom, 27 | ], 28 | }; 29 | -------------------------------------------------------------------------------- /src/commaListsOr/commaListsOr.test.js: -------------------------------------------------------------------------------- 1 | import commaListsOr from './commaListsOr'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as comma-separated list with "or"', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'commaListsOr'); 9 | const actual = commaListsOr` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | 16 | test('only returns the first item of a single element array', () => { 17 | const fruits = ['apple']; 18 | const expected = readFromFixture(__dirname, 'commaListsOrSingleItem'); 19 | const actual = commaListsOr` 20 | Doge <3's these fruits: ${fruits} 21 | they are ${val} 22 | `; 23 | expect(actual).toBe(expected); 24 | }); 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: ['eslint:recommended', 'plugin:prettier/recommended'], 6 | parserOptions: { 7 | ecmaVersion: 2018, 8 | sourceType: 'module', 9 | }, 10 | parser: 'babel-eslint', 11 | 12 | globals: { 13 | console: true, 14 | }, 15 | 16 | rules: { 17 | strict: [2, 'global'], 18 | 19 | 'no-param-reassign': 2, 20 | }, 21 | 22 | overrides: [ 23 | { 24 | files: ['.babelrc.js', '.eslintrc.js', 'jest.config.js'], 25 | 26 | parserOptions: { 27 | sourceType: 'script', 28 | }, 29 | 30 | env: { 31 | node: true, 32 | }, 33 | }, 34 | { 35 | files: '*.test.js', 36 | 37 | env: { 38 | es6: true, 39 | jest: true, 40 | node: true, 41 | }, 42 | }, 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /src/commaListsAnd/commaListsAnd.test.js: -------------------------------------------------------------------------------- 1 | import commaListsAnd from './commaListsAnd'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as comma-separated list with "and"', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'commaListsAnd'); 9 | const actual = commaListsAnd` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | 16 | test('only returns the first item of a single element array', () => { 17 | const fruits = ['apple']; 18 | const expected = readFromFixture(__dirname, 'commaListsAndSingleItem'); 19 | const actual = commaListsAnd` 20 | Doge <3's these fruits: ${fruits} 21 | they are ${val} 22 | `; 23 | expect(actual).toBe(expected); 24 | }); 25 | -------------------------------------------------------------------------------- /src/TemplateTag/TemplateTag.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | 3 | let deprecationWarningPrinted = false; 4 | 5 | /** 6 | * @class TemplateTag 7 | * @classdesc Consumes a pipeline of composable transformer plugins and produces a template tag. 8 | */ 9 | export default class TemplateTag { 10 | /** 11 | * constructs a template tag 12 | * @constructs TemplateTag 13 | * @param {...Object} [...transformers] - an array or arguments list of transformers 14 | * @return {Function} - a template tag 15 | */ 16 | constructor(...transformers) { 17 | if (!deprecationWarningPrinted) { 18 | // eslint-disable-next-line no-console 19 | console.warn( 20 | 'TemplateTag is deprecated and will be removed in the next major version. Use createTag instead.', 21 | ); 22 | deprecationWarningPrinted = true; 23 | } 24 | 25 | return createTag(...transformers); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/index.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import mm from 'micromatch'; 4 | 5 | const observe = ['*', '!index.js', '!index.test.js']; 6 | 7 | const context = {}; 8 | 9 | beforeEach(() => { 10 | context.modules = mm(fs.readdirSync(__dirname), observe); 11 | }); 12 | 13 | function requireModule(module) { 14 | return require(path.join(__dirname, module)); 15 | } 16 | 17 | test('utils exports all the right modules directly', () => { 18 | const modules = context.modules; 19 | expect.assertions(modules.length); 20 | modules.forEach((module) => { 21 | expect(requireModule(module)).toBeDefined(); 22 | }); 23 | }); 24 | 25 | test('utils exports all the right modules as props', () => { 26 | const modules = context.modules; 27 | expect.assertions(modules.length); 28 | modules.forEach((module) => { 29 | expect(require('./index')).toHaveProperty(module, requireModule(module)); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/oneLineCommaListsOr/oneLineCommaListsOr.test.js: -------------------------------------------------------------------------------- 1 | import oneLineCommaListsOr from './oneLineCommaListsOr'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as comma-separated list on one line with "or"', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'oneLineCommaListsOr').trim(); 9 | const actual = oneLineCommaListsOr` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | 16 | test('only returns the first item of a single element array', () => { 17 | const fruits = ['apple']; 18 | const expected = readFromFixture( 19 | __dirname, 20 | 'oneLineCommaListsOrSingleItem', 21 | ).trim(); 22 | const actual = oneLineCommaListsOr` 23 | Doge <3's these fruits: ${fruits} 24 | they are ${val} 25 | `; 26 | expect(actual).toBe(expected); 27 | }); 28 | -------------------------------------------------------------------------------- /src/testUtils/index.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import mm from 'micromatch'; 4 | 5 | const observe = ['*', '!index.js', '!index.test.js']; 6 | 7 | const context = {}; 8 | 9 | beforeEach(() => { 10 | context.modules = mm(fs.readdirSync(__dirname), observe); 11 | }); 12 | 13 | function requireModule(module) { 14 | return require(path.join(__dirname, module)); 15 | } 16 | 17 | test('test utils exports all the right modules directly', () => { 18 | const modules = context.modules; 19 | expect.assertions(modules.length); 20 | modules.forEach((module) => { 21 | expect(requireModule(module)).toBeDefined(); 22 | }); 23 | }); 24 | 25 | test('test utils exports all the right modules as props', () => { 26 | const modules = context.modules; 27 | expect.assertions(modules.length); 28 | modules.forEach((module) => { 29 | expect(require('./index')).toHaveProperty(module, requireModule(module)); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/oneLineCommaListsAnd/oneLineCommaListsAnd.test.js: -------------------------------------------------------------------------------- 1 | import oneLineCommaListsAnd from './oneLineCommaListsAnd'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('includes arrays as comma-separated list on one line with "and"', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'oneLineCommaListsAnd').trim(); 9 | const actual = oneLineCommaListsAnd` 10 | Doge <3's these fruits: ${fruits} 11 | they are ${val} 12 | `; 13 | expect(actual).toBe(expected); 14 | }); 15 | 16 | test('only returns the first item of a single element array', () => { 17 | const fruits = ['apple']; 18 | const expected = readFromFixture( 19 | __dirname, 20 | 'oneLineCommaListsAndSingleItem', 21 | ).trim(); 22 | const actual = oneLineCommaListsAnd` 23 | Doge <3's these fruits: ${fruits} 24 | they are ${val} 25 | `; 26 | expect(actual).toBe(expected); 27 | }); 28 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import mm from 'micromatch'; 4 | 5 | const observe = ['*', '!utils', '!testUtils', '!index.js', '!index.test.js']; 6 | 7 | const context = {}; 8 | 9 | beforeEach(() => { 10 | context.modules = mm(fs.readdirSync(__dirname), observe); 11 | }); 12 | 13 | function requireModule(module) { 14 | return require(path.join(__dirname, module)); 15 | } 16 | 17 | test('common-tags exports all the right modules directly', () => { 18 | const modules = context.modules; 19 | expect.assertions(modules.length); 20 | modules.forEach((module) => { 21 | expect(requireModule(module)).toBeDefined(); 22 | }); 23 | }); 24 | 25 | test('common-tags exports all the right modules as props', () => { 26 | const modules = context.modules; 27 | expect.assertions(modules.length); 28 | modules.forEach((module) => { 29 | expect(require('./index')).toHaveProperty(module, requireModule(module)); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | License (MIT) 2 | ------------- 3 | 4 | Copyright © Declan de Wet 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /src/oneLine/oneLine.test.js: -------------------------------------------------------------------------------- 1 | import oneLine from './oneLine'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('reduces text to one line, replacing newlines with spaces', () => { 7 | const expected = readFromFixture(__dirname, 'oneLine').trim(); 8 | const actual = oneLine` 9 | wow such doge 10 | is very ${val} 11 | at one line neat 12 | from multiline 13 | `; 14 | expect(actual).toBe(expected); 15 | }); 16 | 17 | test('reduces text to one line, replacing newlines with spaces (no indentation)', () => { 18 | const expected = readFromFixture(__dirname, 'oneLine').trim(); 19 | const actual = oneLine` 20 | wow such doge 21 | is very ${val} 22 | at one line neat 23 | 24 | from multiline 25 | `; 26 | expect(actual).toBe(expected); 27 | }); 28 | 29 | test('preserves whitespace within input lines, replacing only newlines', () => { 30 | const expected = readFromFixture(__dirname, 'oneLine-sentence').trim(); 31 | const actual = oneLine` 32 | Sentences also work. Double 33 | spacing is preserved. 34 | `; 35 | expect(actual).toBe(expected); 36 | }); 37 | -------------------------------------------------------------------------------- /src/trimResultTransformer/trimResultTransformer.js: -------------------------------------------------------------------------------- 1 | const supportedSides = ['', 'start', 'left', 'end', 'right', 'smart']; 2 | 3 | /** 4 | * TemplateTag transformer that trims whitespace on the end result of a tagged template 5 | * @param {String} side = '' - The side of the string to trim. Can be 'start' or 'end' (alternatively 'left' or 'right') 6 | * @return {Object} - a TemplateTag transformer 7 | */ 8 | const trimResultTransformer = (side = '') => { 9 | if (!supportedSides.includes(side)) { 10 | throw new Error(`Side not supported: ${side}`); 11 | } 12 | 13 | return { 14 | onEndResult(endResult) { 15 | switch (side) { 16 | case '': 17 | return endResult.trim(); 18 | 19 | case 'start': 20 | case 'left': 21 | return endResult.replace(/^\s*/, ''); 22 | 23 | case 'end': 24 | case 'right': 25 | return endResult.replace(/\s*$/, ''); 26 | 27 | case 'smart': 28 | return endResult.replace(/[^\S\n]+$/gm, '').replace(/^\n/, ''); 29 | } 30 | }, 31 | }; 32 | }; 33 | 34 | export default trimResultTransformer; 35 | -------------------------------------------------------------------------------- /src/removeNonPrintingValuesTransformer/removeNonPrintingValuesTransformer.test.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import inlineArrayTransformer from '../inlineArrayTransformer'; 3 | import removeNonPrintingValuesTransformer from '../removeNonPrintingValuesTransformer'; 4 | 5 | test('removes null', () => { 6 | const remove = createTag(removeNonPrintingValuesTransformer()); 7 | const nil = null; 8 | expect(remove`a${nil}z`).toBe('az'); 9 | }); 10 | 11 | test('removes bool', () => { 12 | const remove = createTag(removeNonPrintingValuesTransformer()); 13 | const yep = true; 14 | const nope = false; 15 | expect(remove`a${yep}${nope}z`).toBe('az'); 16 | }); 17 | 18 | test('removes NaN', () => { 19 | const remove = createTag(removeNonPrintingValuesTransformer()); 20 | const nan = 0 / 0; 21 | expect(remove`a${nan}z`).toBe('az'); 22 | }); 23 | 24 | test('removes non-printing array values', () => { 25 | const remove = createTag( 26 | removeNonPrintingValuesTransformer(), 27 | inlineArrayTransformer(), 28 | ); 29 | const val = ['foo', undefined, 'bar', null]; 30 | expect(remove`a ${val} z`).toBe('a foo bar z'); 31 | }); 32 | -------------------------------------------------------------------------------- /src/stripIndentTransformer/stripIndentTransformer.js: -------------------------------------------------------------------------------- 1 | const supportedTypes = ['initial', 'all']; 2 | 3 | /** 4 | * strips indentation from a template literal 5 | * @param {String} type = 'initial' - whether to remove all indentation or just leading indentation. can be 'all' or 'initial' 6 | * @return {Object} - a TemplateTag transformer 7 | */ 8 | const stripIndentTransformer = (type = 'initial') => { 9 | if (!supportedTypes.includes(type)) { 10 | throw new Error(`Type not supported: ${type}`); 11 | } 12 | 13 | return { 14 | onEndResult(endResult) { 15 | if (type === 'all') { 16 | // remove all indentation from each line 17 | return endResult.replace(/^[^\S\n]+/gm, ''); 18 | } 19 | 20 | // remove the shortest leading indentation from each line 21 | const match = endResult.match(/^[^\S\n]*(?=\S)/gm); 22 | const indent = match && Math.min(...match.map((el) => el.length)); 23 | if (indent) { 24 | const regexp = new RegExp(`^.{${indent}}`, 'gm'); 25 | return endResult.replace(regexp, ''); 26 | } 27 | return endResult; 28 | }, 29 | }; 30 | }; 31 | 32 | export default stripIndentTransformer; 33 | -------------------------------------------------------------------------------- /src/splitStringTransformer/splitStringTransformer.test.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import inlineArrayTransformer from '../inlineArrayTransformer'; 3 | import splitStringTransformer from './splitStringTransformer'; 4 | 5 | test('splits a string substitution into an array by the specified character', () => { 6 | const tag = createTag(splitStringTransformer('\n'), inlineArrayTransformer()); 7 | expect(tag`foo ${'bar\nbaz'}`).toBe('foo bar baz'); 8 | }); 9 | 10 | test('ignores string if splitBy character is not found', () => { 11 | const tag = createTag(splitStringTransformer('.')); 12 | expect(tag`foo ${'bar,baz'}`).toBe('foo bar,baz'); 13 | }); 14 | 15 | test('ignores substitution if it is not a string', () => { 16 | const tag = createTag(splitStringTransformer('')); 17 | expect(tag`foo ${5}`).toBe('foo 5'); 18 | }); 19 | 20 | test('throws an error if splitBy param is undefined', () => { 21 | expect(() => { 22 | splitStringTransformer(); 23 | }).toThrow(/specify a string character to split by/); 24 | }); 25 | 26 | test('throws an error if splitBy param is not a string', () => { 27 | expect(() => { 28 | splitStringTransformer(42); 29 | }).toThrow(/specify a string character to split by/); 30 | }); 31 | -------------------------------------------------------------------------------- /src/replaceResultTransformer/replaceResultTransformer.test.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import replaceResultTransformer from './replaceResultTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | 5 | test('replaces sequential whitespace with a single space', () => { 6 | const oneLine = createTag( 7 | replaceResultTransformer(/(?:\s+)/g, ' '), 8 | trimResultTransformer(), 9 | ); 10 | const expected = 'foo bar baz'; 11 | const actual = oneLine` 12 | foo 13 | bar 14 | baz 15 | `; 16 | expect(actual).toBe(expected); 17 | }); 18 | 19 | test('can be set so sequence requires a newline at the beginning before triggering replacement', () => { 20 | const oneLineTrim = createTag( 21 | replaceResultTransformer(/(?:\n\s+)/g, ''), 22 | trimResultTransformer(), 23 | ); 24 | const expected = 'https://google.com?utm_source=common-tags'; 25 | const actual = oneLineTrim` 26 | https:// 27 | google.com 28 | ?utm_source=common-tags 29 | `; 30 | expect(actual).toBe(expected); 31 | }); 32 | 33 | test('throws error if no arguments are supplied', () => { 34 | expect(() => { 35 | replaceResultTransformer(); 36 | }).toThrow(/requires exactly 2 arguments/); 37 | }); 38 | -------------------------------------------------------------------------------- /src/safeHtml/safeHtml.test.js: -------------------------------------------------------------------------------- 1 | import safeHtml from './safeHtml'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('renders HTML, including arrays', () => { 7 | const fruits = ['apple', 'banana', 'kiwi']; 8 | const expected = readFromFixture(__dirname, 'normal-html'); 9 | const actual = safeHtml` 10 |

${val}

11 | 14 | `; 15 | expect(actual).toBe(expected); 16 | }); 17 | 18 | test('converts strings containing newlines into proper indented output', () => { 19 | const newlines = 'one\ntwo'; 20 | const expected = readFromFixture(__dirname, 'newline-conversion'); 21 | const actual = safeHtml` 22 |

${val}

23 | 27 | `; 28 | expect(actual).toBe(expected); 29 | }); 30 | 31 | test('correctly escapes HTML tags on substitution', () => { 32 | const fruits = ['apple', 'banana', 'kiwi', '

dangerous fruit

']; 33 | const expected = readFromFixture(__dirname, 'escaped-html'); 34 | const actual = safeHtml` 35 |

${val}

36 | 39 | `; 40 | expect(actual).toBe(expected); 41 | }); 42 | -------------------------------------------------------------------------------- /src/replaceSubstitutionTransformer/replaceSubstitutionTransformer.test.js: -------------------------------------------------------------------------------- 1 | import replaceSubstitutionTransformer from './replaceSubstitutionTransformer'; 2 | import createTag from '../createTag'; 3 | 4 | test('only operates on substitutions', () => { 5 | const tag = createTag( 6 | replaceSubstitutionTransformer(//g, '>'), 8 | ); 9 | expect(tag`

foo${''}

`).toBe( 10 | '

foo<bar></bar>

', 11 | ); 12 | }); 13 | 14 | test('does not touch undefined and null substitutions', () => { 15 | const tag = createTag(replaceSubstitutionTransformer(/u/g, '')); 16 | expect(tag`foo ${undefined} bar ${null}`).toBe('foo undefined bar null'); 17 | }); 18 | 19 | test('works on numbers', () => { 20 | const tag = createTag(replaceSubstitutionTransformer(/2/g, '3')); 21 | expect(tag`foo ${2} bar ${43.12}`).toBe('foo 3 bar 43.13'); 22 | }); 23 | 24 | test('works on arrays', () => { 25 | const tag = createTag(replaceSubstitutionTransformer(/foo/g, 'bar')); 26 | expect(tag`${['foo', 'bar', 'foo']}`).toBe('bar,bar,bar'); 27 | }); 28 | 29 | test('throws error if no arguments are supplied', () => { 30 | expect(() => { 31 | replaceSubstitutionTransformer(); 32 | }).toThrow(/requires exactly 2 arguments/); 33 | }); 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // core 2 | export createTag from './createTag'; 3 | 4 | // transformers 5 | export inlineArrayTransformer from './inlineArrayTransformer'; 6 | export removeNonPrintingValuesTransformer from './removeNonPrintingValuesTransformer'; 7 | export replaceResultTransformer from './replaceResultTransformer'; 8 | export replaceStringTransformer from './replaceStringTransformer'; 9 | export replaceSubstitutionTransformer from './replaceSubstitutionTransformer'; 10 | export splitStringTransformer from './splitStringTransformer'; 11 | export stripIndentTransformer from './stripIndentTransformer'; 12 | export trimResultTransformer from './trimResultTransformer'; 13 | 14 | // tags 15 | export codeBlock from './codeBlock'; 16 | export commaLists from './commaLists'; 17 | export commaListsAnd from './commaListsAnd'; 18 | export commaListsOr from './commaListsOr'; 19 | export html from './html'; 20 | export id from './id'; 21 | export inlineLists from './inlineLists'; 22 | export oneLine from './oneLine'; 23 | export oneLineCommaLists from './oneLineCommaLists'; 24 | export oneLineCommaListsAnd from './oneLineCommaListsAnd'; 25 | export oneLineCommaListsOr from './oneLineCommaListsOr'; 26 | export oneLineInlineLists from './oneLineInlineLists'; 27 | export oneLineTrim from './oneLineTrim'; 28 | export safeHtml from './safeHtml'; 29 | export source from './source'; 30 | export stripIndent from './stripIndent'; 31 | export stripIndents from './stripIndents'; 32 | 33 | // deprecated 34 | export TemplateTag from './TemplateTag'; 35 | -------------------------------------------------------------------------------- /src/stripIndent/stripIndent.test.js: -------------------------------------------------------------------------------- 1 | import stripIndent from './stripIndent'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | 6 | test('strips indentation', () => { 7 | const expected = readFromFixture(__dirname, 'stripIndent'); 8 | const actual = stripIndent` 9 | wow such indent gone 10 | very ${val} 11 | `; 12 | expect(actual).toBe(expected); 13 | }); 14 | 15 | test('strips larger indentation', () => { 16 | const expected = readFromFixture(__dirname, 'stripIndent'); 17 | const actual = stripIndent` 18 | wow such indent gone 19 | very ${val} 20 | `; 21 | expect(actual).toBe(expected); 22 | }); 23 | 24 | test('maintains deeper indentation', () => { 25 | const expected = readFromFixture(__dirname, 'maintainIndent'); 26 | const actual = stripIndent` 27 | wow such indent gone 28 | very ${val} 29 | `; 30 | expect(actual).toBe(expected); 31 | }); 32 | 33 | test('maintains empty lines', () => { 34 | const expected = readFromFixture(__dirname, 'maintainEmptyLines'); 35 | const actual = stripIndent` 36 | wow such indent gone 37 | 38 | very ${val} 39 | `; 40 | expect(actual).toBe(expected); 41 | }); 42 | 43 | test('does nothing if there are no indents', () => { 44 | const expected = 'wow such doge'; 45 | const actual = stripIndent`wow such doge`; 46 | expect(actual).toBe(expected); 47 | }); 48 | 49 | test('does nothing if minimal indent has zero length', () => { 50 | const expected = 'wow\n such\n doge'; 51 | const actual = stripIndent`wow\n such\n doge`; 52 | expect(actual).toBe(expected); 53 | }); 54 | -------------------------------------------------------------------------------- /src/TemplateTag/TemplateTag.test.js: -------------------------------------------------------------------------------- 1 | import TemplateTag from '../TemplateTag'; 2 | 3 | /* eslint-disable no-console */ 4 | 5 | beforeEach(() => { 6 | console.warn = jest.fn(); 7 | }); 8 | 9 | test('a warning should be printed the first time a TemplateTag is constructed', () => { 10 | expect(console.warn).toHaveBeenCalledTimes(0); 11 | 12 | new TemplateTag(); 13 | 14 | expect(console.warn).toHaveBeenCalledTimes(1); 15 | expect(console.warn).toHaveBeenCalledWith( 16 | expect.stringContaining('Use createTag instead'), 17 | ); 18 | 19 | new TemplateTag(); 20 | 21 | expect(console.warn).toHaveBeenCalledTimes(1); 22 | }); 23 | 24 | /* eslint-enable no-console */ 25 | 26 | test('performs a transformation & provides correct values to transform methods', () => { 27 | const tag = new TemplateTag({ 28 | onString(str) { 29 | this.ctx = this.ctx || { strings: [], subs: [] }; 30 | this.ctx.strings.push(str); 31 | return str; 32 | }, 33 | onSubstitution(substitution, resultSoFar) { 34 | this.ctx.subs.push({ substitution, resultSoFar }); 35 | return substitution; 36 | }, 37 | onEndResult(endResult) { 38 | this.ctx.endResult = endResult.toUpperCase(); 39 | return this.ctx; 40 | }, 41 | }); 42 | const data = tag`foo ${'bar'} baz ${'fizz'}`; 43 | expect(data).toEqual({ 44 | endResult: 'FOO BAR BAZ FIZZ', 45 | strings: ['foo ', ' baz ', ''], 46 | subs: [ 47 | { 48 | substitution: 'bar', 49 | resultSoFar: 'foo ', 50 | }, 51 | { 52 | substitution: 'fizz', 53 | resultSoFar: 'foo bar baz ', 54 | }, 55 | ], 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/stripIndentTransformer/stripIndentTransformer.test.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndentTransformer from './stripIndentTransformer'; 3 | import trimResultTransformer from '../trimResultTransformer'; 4 | import { readFromFixture } from '../testUtils'; 5 | 6 | test('default behaviour removes the leading indent, but preserves the rest', () => { 7 | const stripIndent = createTag( 8 | stripIndentTransformer(), 9 | trimResultTransformer('smart'), 10 | ); 11 | const expected = readFromFixture(__dirname, 'stripIndent'); 12 | const actual = stripIndent` 13 | foo bar baz 14 | bar baz foo 15 | baz foo bar 16 | wow such doge 17 | `; 18 | expect(actual).toBe(expected); 19 | }); 20 | 21 | test('type "initial" does not remove indents if there is no need to do so', () => { 22 | const stripIndent = createTag( 23 | stripIndentTransformer(), 24 | trimResultTransformer('smart'), 25 | ); 26 | expect(stripIndent``).toBe(''); 27 | expect(stripIndent`foo`).toBe('foo'); 28 | expect(stripIndent`foo\nbar`).toBe('foo\nbar'); 29 | }); 30 | 31 | test('removes all indents if type is "all"', () => { 32 | const stripIndents = createTag( 33 | stripIndentTransformer('all'), 34 | trimResultTransformer('smart'), 35 | ); 36 | const expected = readFromFixture(__dirname, 'stripIndents'); 37 | const actual = stripIndents` 38 | foo bar baz 39 | bar baz foo 40 | baz foo bar 41 | wow such doge 42 | `; 43 | expect(actual).toBe(expected); 44 | }); 45 | 46 | test('throws an error if encounters invalid type', () => { 47 | expect(() => { 48 | stripIndentTransformer('blue'); 49 | }).toThrow(/not supported/); 50 | }); 51 | -------------------------------------------------------------------------------- /src/inlineArrayTransformer/inlineArrayTransformer.js: -------------------------------------------------------------------------------- 1 | import { prefixLines, stripLastNewLine } from '../utils'; 2 | 3 | /** 4 | * Converts an array substitution to a string containing a list 5 | * @param {String} [opts.separator = ''] - The character that separates each item 6 | * @param {String} [opts.conjunction = ''] - Replace the last separator with this 7 | * @param {Boolean} [opts.serial = false] - Include the separator before the conjunction? (Oxford comma use-case) 8 | * 9 | * @return {Object} - A transformer 10 | */ 11 | const inlineArrayTransformer = ({ 12 | conjunction = '', 13 | separator = '', 14 | serial = false, 15 | } = {}) => ({ 16 | onSubstitution(substitution, resultSoFar) { 17 | // only operate on arrays 18 | if (!Array.isArray(substitution)) { 19 | return substitution; 20 | } 21 | 22 | const { length } = substitution; 23 | const lastSeparatorIndex = conjunction && !serial ? length - 2 : length - 1; 24 | const indentation = resultSoFar.match(/(?:\n)([^\S\n]+)$/); 25 | 26 | if (conjunction && length > 1) { 27 | substitution[length - 1] = ''.concat( 28 | conjunction, 29 | ' ', 30 | substitution[length - 1], 31 | ); 32 | } 33 | 34 | return substitution.reduce((result, part, index) => { 35 | const isFirstPart = index === 0; 36 | const strippedPart = stripLastNewLine(part); 37 | return ''.concat( 38 | result, 39 | isFirstPart ? '' : indentation ? '\n' : ' ', 40 | indentation 41 | ? prefixLines(indentation[1], strippedPart, isFirstPart) 42 | : strippedPart, 43 | index < lastSeparatorIndex ? separator : '', 44 | ); 45 | }, ''); 46 | }, 47 | }); 48 | 49 | export default inlineArrayTransformer; 50 | -------------------------------------------------------------------------------- /src/trimResultTransformer/trimResultTransformer.test.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | import stripIndentTransformer from '../stripIndentTransformer'; 3 | import trimResultTransformer from './trimResultTransformer'; 4 | 5 | test('trims outer padding', () => { 6 | const trim = createTag(trimResultTransformer()); 7 | expect(trim` foo `).toBe('foo'); 8 | }); 9 | 10 | test('trims start padding', () => { 11 | const trimStart = createTag(trimResultTransformer('start')); 12 | expect(trimStart` foo `).toBe('foo '); 13 | }); 14 | 15 | test('trims left padding', () => { 16 | const trimLeft = createTag(trimResultTransformer('left')); 17 | expect(trimLeft` foo `).toBe('foo '); 18 | }); 19 | 20 | test('trims end padding', () => { 21 | const trimEnd = createTag(trimResultTransformer('end')); 22 | expect(trimEnd` foo `).toBe(' foo'); 23 | }); 24 | 25 | test('trims right padding', () => { 26 | const trimRight = createTag(trimResultTransformer('right')); 27 | expect(trimRight` foo `).toBe(' foo'); 28 | }); 29 | 30 | test('can be used sequentially', () => { 31 | const trimStart = createTag( 32 | stripIndentTransformer(), 33 | trimResultTransformer('start'), 34 | ); 35 | expect(trimStart` foo `).toBe('foo '); 36 | expect(trimStart` bar `).toBe('bar '); 37 | }); 38 | 39 | describe('smart trimming', () => { 40 | const trimSmart = createTag(trimResultTransformer('smart')); 41 | 42 | test('leaves a string without surrounding whitespace as-is', () => { 43 | expect(trimSmart`a`).toBe('a'); 44 | }); 45 | 46 | test('performs an end-side trim on a single-line string', () => { 47 | expect(trimSmart` a `).toBe(' a'); 48 | }); 49 | 50 | test('trims whitespace at the end of each line', () => { 51 | expect(trimSmart`a \n b \nc `).toBe('a\n b\nc'); 52 | }); 53 | 54 | test("removes the first line if it's empty", () => { 55 | expect(trimSmart` \na`).toBe('a'); 56 | }); 57 | 58 | test('leaves the trailing newline character', () => { 59 | expect(trimSmart`a \n`).toBe('a\n'); 60 | }); 61 | 62 | test("doesn't remove intentional newline characters", () => { 63 | expect(trimSmart`a\n \n`).toBe('a\n\n'); 64 | }); 65 | }); 66 | 67 | test('throws an error if invalid side supplied', () => { 68 | expect(() => { 69 | trimResultTransformer('up'); 70 | }).toThrow(/not supported/); 71 | }); 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common-tags", 3 | "description": "a few common utility template tags for ES2015", 4 | "version": "2.0.0-alpha.1", 5 | "author": "Declan de Wet ", 6 | "bugs": { 7 | "url": "http://github.com/declandewet/common-tags/issues" 8 | }, 9 | "contributors": [ 10 | "Declan de Wet (https://github.com/declandewet)", 11 | "Jason Killian (https://github.com/JKillian)", 12 | "Laurent Goudet (https://github.com/laurentgoudet)", 13 | "Kamil Ogórek (https://github.com/kamilogorek)", 14 | "Lucian Buzzo (https://github.com/LucianBuzzo)", 15 | "Rafał Ruciński (https://github.com/fatfisz)" 16 | ], 17 | "devDependencies": { 18 | "@babel/cli": "^7.10.1", 19 | "@babel/core": "^7.10.2", 20 | "@babel/plugin-proposal-class-properties": "^7.10.1", 21 | "@babel/plugin-proposal-export-default-from": "^7.10.1", 22 | "@babel/preset-env": "^7.10.2", 23 | "@rollup/plugin-babel": "^5.0.2", 24 | "@rollup/plugin-node-resolve": "^8.0.0", 25 | "babel-core": "^6.26.3", 26 | "babel-eslint": "^10.1.0", 27 | "babel-plugin-add-module-exports": "^1.0.2", 28 | "codecov": "^3.7.0", 29 | "cross-env": "^7.0.2", 30 | "doctoc": "^1.4.0", 31 | "eslint": "^7.1.0", 32 | "eslint-config-prettier": "^6.11.0", 33 | "eslint-plugin-prettier": "^3.1.3", 34 | "jest": "^26.0.1", 35 | "micromatch": "^4.0.2", 36 | "prettier": "^2.0.5", 37 | "rimraf": "^3.0.2", 38 | "rollup": "^2.12.0", 39 | "rollup-plugin-uglify": "^6.0.4" 40 | }, 41 | "directories": { 42 | "lib": "lib" 43 | }, 44 | "engines": { 45 | "node": ">=10.0.0" 46 | }, 47 | "homepage": "https://github.com/declandewet/common-tags", 48 | "keywords": [ 49 | "array", 50 | "babel", 51 | "es2015", 52 | "es2015-tag", 53 | "es6", 54 | "es6-tag", 55 | "heredoc", 56 | "html", 57 | "indent", 58 | "indents", 59 | "line", 60 | "literal", 61 | "multi", 62 | "multiline", 63 | "normalize", 64 | "one", 65 | "oneline", 66 | "single", 67 | "singleline", 68 | "string", 69 | "strings", 70 | "strip", 71 | "tag", 72 | "tagged", 73 | "template" 74 | ], 75 | "license": "MIT", 76 | "main": "lib", 77 | "jsnext:main": "es", 78 | "module": "es", 79 | "unpkg": "dist/common-tags.min.js", 80 | "files": [ 81 | "/dist", 82 | "/es", 83 | "/lib" 84 | ], 85 | "repository": { 86 | "type": "git", 87 | "url": "https://github.com/declandewet/common-tags" 88 | }, 89 | "scripts": { 90 | "clear": "rimraf lib && rimraf es && rimraf dist", 91 | "build": "npm run clear && npm run build:cjs && npm run build:es && npm run build:unpkg", 92 | "build:cjs": "babel src --out-dir lib --ignore **/*.test.js", 93 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es --ignore **/*.test.js", 94 | "build:unpkg": "cross-env BABEL_ENV=es rollup --config", 95 | "codecov": "codecov", 96 | "doctoc": "doctoc readme.md --title \"## Table of Contents\"", 97 | "lint": "eslint .*rc.js *.js src/**/*.js --ignore-pattern '!.*rc.js'", 98 | "lint:fix": "npm run lint -- --fix", 99 | "prerelease": "npm run build", 100 | "preversion": "npm run doctoc && npm test", 101 | "release": "npm publish", 102 | "test": "npm run lint && jest src" 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/inlineArrayTransformer/inlineArrayTransformer.test.js: -------------------------------------------------------------------------------- 1 | import inlineArrayTransformer from './inlineArrayTransformer'; 2 | import createTag from '../createTag'; 3 | 4 | test('only operates on arrays', () => { 5 | const tag = createTag(inlineArrayTransformer); 6 | expect(tag`foo ${5} ${'bar'}`).toBe('foo 5 bar'); 7 | }); 8 | 9 | test('includes an array as a comma-separated list', () => { 10 | const tag = createTag(inlineArrayTransformer({ separator: ',' })); 11 | expect(tag`I like ${['apple', 'banana', 'kiwi']}`).toBe( 12 | 'I like apple, banana, kiwi', 13 | ); 14 | }); 15 | 16 | test('replaces last separator with a conjunction', () => { 17 | const tag = createTag( 18 | inlineArrayTransformer({ separator: ',', conjunction: 'and' }), 19 | ); 20 | expect(tag`I like ${['apple', 'banana', 'kiwi']}`).toBe( 21 | 'I like apple, banana and kiwi', 22 | ); 23 | }); 24 | 25 | test('replaces last separator with a conjunction', () => { 26 | const tag = createTag( 27 | inlineArrayTransformer({ separator: ',', conjunction: 'and' }), 28 | ); 29 | expect( 30 | tag`I like ${['apple', 'banana', 'a fruit that has "," in the name']}`, 31 | ).toBe('I like apple, banana and a fruit that has "," in the name'); 32 | }); 33 | 34 | test('does not use a conjunction if there is only one item in an array', () => { 35 | const tag = createTag( 36 | inlineArrayTransformer({ separator: ',', conjunction: 'and' }), 37 | ); 38 | expect(tag`I like ${['apple']}`).toBe('I like apple'); 39 | }); 40 | 41 | test('does not require preceded whitespace', () => { 42 | const tag = createTag(inlineArrayTransformer({ separator: ',' })); 43 | expect(tag`My friends are (${['bob', 'sally', 'jim']})`).toBe( 44 | 'My friends are (bob, sally, jim)', 45 | ); 46 | }); 47 | 48 | test('supports serial/oxford separators', () => { 49 | const tag = createTag( 50 | inlineArrayTransformer({ separator: ',', conjunction: 'or', serial: true }), 51 | ); 52 | expect(tag`My friends are always ${['dramatic', 'emotional', 'needy']}`).toBe( 53 | 'My friends are always dramatic, emotional, or needy', 54 | ); 55 | }); 56 | 57 | test('maintains indentation', () => { 58 | const tag = createTag(inlineArrayTransformer()); 59 | expect(tag`My friends are always 60 | ${['dramatic', 'emotional', 'needy']}`).toBe( 61 | 'My friends are always\n dramatic\n emotional\n needy', 62 | ); 63 | }); 64 | 65 | test('maintains indentation in multiline strings', () => { 66 | const tag = createTag(inlineArrayTransformer({ separator: ',' })); 67 | expect(tag`My friends are always 68 | ${['dra-\nmatic', 'emo-\ntional', 'nee-\ndy']}`).toBe( 69 | 'My friends are always\n dra-\n matic,\n emo-\n tional,\n nee-\n dy', 70 | ); 71 | }); 72 | 73 | test('maintains indentation in multiline strings (with conjunction)', () => { 74 | const tag = createTag( 75 | inlineArrayTransformer({ separator: ',', conjunction: 'and' }), 76 | ); 77 | expect(tag`My friends are always 78 | ${['dra-\nmatic', 'emo-\ntional', 'nee-\ndy']}`).toBe( 79 | 'My friends are always\n dra-\n matic,\n emo-\n tional\n and nee-\n dy', 80 | ); 81 | }); 82 | 83 | test('maintains indentation in multiline strings (with serial/oxford separators)', () => { 84 | const tag = createTag( 85 | inlineArrayTransformer({ 86 | separator: ',', 87 | conjunction: 'and', 88 | serial: true, 89 | }), 90 | ); 91 | expect(tag`My friends are always 92 | ${['dra-\nmatic', 'emo-\ntional', 'nee-\ndy']}`).toBe( 93 | 'My friends are always\n dra-\n matic,\n emo-\n tional,\n and nee-\n dy', 94 | ); 95 | }); 96 | 97 | test('does not introduce excess newlines', () => { 98 | const tag = createTag(inlineArrayTransformer()); 99 | expect(tag`My friends are always 100 | 101 | ${['dramatic', 'emotional', 'needy']}`).toBe( 102 | 'My friends are always\n\n dramatic\n emotional\n needy', 103 | ); 104 | }); 105 | -------------------------------------------------------------------------------- /src/html/html.test.js: -------------------------------------------------------------------------------- 1 | import html from './html'; 2 | import { readFromFixture } from '../testUtils'; 3 | 4 | const val = 'amaze'; 5 | const nil = null; 6 | 7 | test('renders HTML, including arrays', () => { 8 | const fruits = ['apple', 'banana', 'kiwi']; 9 | const expected = readFromFixture(__dirname, 'html'); 10 | const actual = html` 11 |

${val}${nil}

12 |
    13 | ${fruits.map((fruit) => `
  • ${fruit}
  • `)} 14 |
15 | `; 16 | 17 | expect(actual).toBe(expected); 18 | }); 19 | 20 | test('converts strings containing newlines into proper indented output', () => { 21 | const newlines = '
  • one
  • \n
  • two
  • '; 22 | const expected = readFromFixture(__dirname, 'newline-conversion'); 23 | const actual = html` 24 |

    ${val}${nil}

    25 |
      26 | ${newlines} 27 |
    • three
    • 28 |
    29 | `; 30 | 31 | expect(actual).toBe(expected); 32 | }); 33 | 34 | test('does not introduce excess newlines', () => { 35 | const newlines = '
  • one
  • \n
  • two
  • '; 36 | const expected = readFromFixture( 37 | __dirname, 38 | 'newline-conversion-after-newline', 39 | ); 40 | /* eslint-disable prettier/prettier */ 41 | const actual = html` 42 |

    ${val}${nil}

    43 |
      44 | 45 | ${newlines} 46 |
    • three
    • 47 |
    48 | `; 49 | /* eslint-enable prettier/prettier */ 50 | 51 | expect(actual).toBe(expected); 52 | }); 53 | 54 | test('renders nested HTML', () => { 55 | const fruits = ['apple', 'banana', 'kiwi']; 56 | const expected = readFromFixture(__dirname, 'nesting'); 57 | 58 | function renderFruit(fruit) { 59 | return html` 60 |
  • 61 |
    ${fruit}
    62 |
  • 63 | `; 64 | } 65 | 66 | const actual = html` 67 | 68 | 69 | 70 |
      71 | ${fruits.map(renderFruit)} 72 |
    73 | 74 | 75 | `; 76 | 77 | expect(actual).toBe(expected); 78 | }); 79 | 80 | test('renders nested HTML without excess empty lines', () => { 81 | const fruits = ['apple', 'banana', 'kiwi']; 82 | const expected = readFromFixture(__dirname, 'nesting-no-excess'); 83 | 84 | function renderFruit(fruit) { 85 | return html` 86 |
  • 87 |
    ${fruit}
    88 |
  • 89 | `; 90 | } 91 | 92 | /* eslint-disable prettier/prettier */ 93 | const actual = html` 94 | 95 | 96 | 97 |
      98 | 99 | ${fruits.map(renderFruit)} 100 | 101 |
    102 | 103 | 104 | `; 105 | /* eslint-enable prettier/prettier */ 106 | 107 | expect(actual).toBe(expected); 108 | }); 109 | 110 | test("just strips indent when there's an empty array inside", () => { 111 | const expected = readFromFixture(__dirname, 'empty-array'); 112 | /* eslint-disable prettier/prettier */ 113 | const actual = html` 114 | 115 | 116 | 117 |
      ${[]}
    118 | 119 | 120 | `; 121 | /* eslint-enable prettier/prettier */ 122 | 123 | expect(actual).toBe(expected); 124 | }); 125 | 126 | test("just strips indent when there's an empty array inside (multiline)", () => { 127 | const expected = readFromFixture(__dirname, 'empty-array-multiline'); 128 | const actual = html` 129 | 130 | 131 | 132 |
      133 | ${[]} 134 |
    135 | 136 | 137 | `; 138 | 139 | expect(actual).toBe(expected); 140 | }); 141 | 142 | test('may not indent as expected when the array is not in a new line', () => { 143 | const fruits = ['apple', 'banana', 'kiwi']; 144 | const expected = readFromFixture(__dirname, 'nesting-improper'); 145 | 146 | function renderFruit(fruit) { 147 | return html` 148 |
  • 149 |
    ${fruit}
    150 |
  • 151 | `; 152 | } 153 | 154 | /* eslint-disable prettier/prettier */ 155 | const actual = html` 156 | 157 | 158 | 159 |
      ${fruits.map(renderFruit)}
    160 | 161 | 162 | `; 163 | /* eslint-enable prettier/prettier */ 164 | 165 | expect(actual).toBe(expected); 166 | }); 167 | -------------------------------------------------------------------------------- /src/createTag/createTag.js: -------------------------------------------------------------------------------- 1 | import { flat } from '../utils'; 2 | 3 | const tagTransformersSymbol = 'COMMON_TAGS_TAG_TRANSFORMERS_SYMBOL'; 4 | 5 | function isTag(fn) { 6 | return typeof fn === 'function' && fn[tagTransformersSymbol]; 7 | } 8 | 9 | function cleanTransformers(transformers) { 10 | return flat(transformers).reduce( 11 | (transformers, transformer) => 12 | isTag(transformer) 13 | ? [...transformers, ...transformer[tagTransformersSymbol]] 14 | : [...transformers, transformer], 15 | [], 16 | ); 17 | } 18 | 19 | /** 20 | * An intermediary template tag that receives a template tag and passes the result of calling the template with the received 21 | * template tag to our own template tag. 22 | * @param {Function} nextTag - The received template tag 23 | * @param {Array} template - The template to process 24 | * @param {...*} ...substitutions - `substitutions` is an array of all substitutions in the template 25 | * @return {*} - The final processed value 26 | */ 27 | function getInterimTag(originalTag, extraTag) { 28 | return function tag(...args) { 29 | return originalTag(['', ''], extraTag(...args)); 30 | }; 31 | } 32 | 33 | function getTagCallInfo(transformers) { 34 | return { 35 | transformers, 36 | context: transformers.map((transformer) => 37 | transformer.getInitialContext ? transformer.getInitialContext() : {}, 38 | ), 39 | }; 40 | } 41 | 42 | /** 43 | * Iterate through each transformer, calling the transformer's specified hook. 44 | * @param {Array} transformers - The transformer functions 45 | * @param {String} hookName - The name of the hook 46 | * @param {String} initialString - The input string 47 | * @return {String} - The final results of applying each transformer 48 | */ 49 | function applyHook0({ transformers, context }, hookName, initialString) { 50 | return transformers.reduce( 51 | (result, transformer, index) => 52 | transformer[hookName] 53 | ? transformer[hookName](result, context[index]) 54 | : result, 55 | initialString, 56 | ); 57 | } 58 | 59 | /** 60 | * Iterate through each transformer, calling the transformer's specified hook. 61 | * @param {Array} transformers - The transformer functions 62 | * @param {String} hookName - The name of the hook 63 | * @param {String} initialString - The input string 64 | * @param {*} arg1 - An additional argument passed to the hook 65 | * @return {String} - The final results of applying each transformer 66 | */ 67 | function applyHook1({ transformers, context }, hookName, initialString, arg1) { 68 | return transformers.reduce( 69 | (result, transformer, index) => 70 | transformer[hookName] 71 | ? transformer[hookName](result, arg1, context[index]) 72 | : result, 73 | initialString, 74 | ); 75 | } 76 | 77 | /** 78 | * Consumes a pipeline of composable transformer plugins and produces a template tag. 79 | * @param {...Object} [...rawTransformers] - An array or arguments list of transformers 80 | * @return {Function} - A template tag 81 | */ 82 | export default function createTag(...rawTransformers) { 83 | const transformers = cleanTransformers(rawTransformers); 84 | 85 | function tag(strings, ...expressions) { 86 | if (typeof strings === 'function') { 87 | // if the first argument passed is a function, assume it is a template tag and return 88 | // an intermediary tag that processes the template using the aforementioned tag, passing the 89 | // result to our tag 90 | return getInterimTag(tag, strings); 91 | } 92 | 93 | if (!Array.isArray(strings)) { 94 | return tag([strings]); 95 | } 96 | 97 | const tagCallInfo = getTagCallInfo(transformers); 98 | 99 | // if the first argument is an array, return a transformed end result of processing the template with our tag 100 | const processedTemplate = strings 101 | .map((string) => applyHook0(tagCallInfo, 'onString', string)) 102 | .reduce((result, string, index) => 103 | ''.concat( 104 | result, 105 | applyHook1( 106 | tagCallInfo, 107 | 'onSubstitution', 108 | expressions[index - 1], 109 | result, 110 | ), 111 | string, 112 | ), 113 | ); 114 | 115 | return applyHook0(tagCallInfo, 'onEndResult', processedTemplate); 116 | } 117 | 118 | tag[tagTransformersSymbol] = transformers; 119 | 120 | return tag; 121 | } 122 | -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Artboard 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/createTag/createTag.test.js: -------------------------------------------------------------------------------- 1 | import createTag from '../createTag'; 2 | 3 | test('does no processing by default', () => { 4 | const tag = createTag(); 5 | expect(tag`foo`).toBe('foo'); 6 | }); 7 | 8 | test('transformer methods are optional', () => { 9 | const noMethods = createTag({}); 10 | const noSubNorEnd = createTag({ 11 | onString(str) { 12 | return str.toUpperCase(); 13 | }, 14 | }); 15 | const noStringNorSub = createTag({ 16 | onEndResult(endResult) { 17 | return endResult.toUpperCase(); 18 | }, 19 | }); 20 | const noStringNorEnd = createTag({ 21 | onSubstitution(sub) { 22 | return sub.split('').reverse().join(''); 23 | }, 24 | }); 25 | expect(noMethods`foo`).toBe('foo'); 26 | expect(noSubNorEnd`foo ${'bar'} baz`).toBe('FOO bar BAZ'); 27 | expect(noStringNorSub`bar`).toBe('BAR'); 28 | expect(noStringNorEnd`foo ${'bar'}`).toBe('foo rab'); 29 | }); 30 | 31 | test('calls hooks with an additional context argument', () => { 32 | const tag = createTag({ 33 | getInitialContext() { 34 | return { strings: [], subs: [] }; 35 | }, 36 | onString(str, context) { 37 | context.strings.push(str); 38 | return str; 39 | }, 40 | onSubstitution(substitution, resultSoFar, context) { 41 | context.subs.push({ substitution, resultSoFar }); 42 | return substitution; 43 | }, 44 | onEndResult(endResult, context) { 45 | context.endResult = endResult.toUpperCase(); 46 | return context; 47 | }, 48 | }); 49 | const data = tag`foo ${'bar'} baz ${'fizz'}`; 50 | expect(data).toEqual({ 51 | endResult: 'FOO BAR BAZ FIZZ', 52 | strings: ['foo ', ' baz ', ''], 53 | subs: [ 54 | { 55 | substitution: 'bar', 56 | resultSoFar: 'foo ', 57 | }, 58 | { 59 | substitution: 'fizz', 60 | resultSoFar: 'foo bar baz ', 61 | }, 62 | ], 63 | }); 64 | }); 65 | 66 | test('each transformer has its own context', () => { 67 | let defaultContext; 68 | const transformerWithDefaultContext = { 69 | onString(str, context) { 70 | context.onStringCalled = true; 71 | }, 72 | onSubstitution(substitution, resultSoFar, context) { 73 | context.onSubstitutionCalled = true; 74 | }, 75 | onEndResult(endResult, context) { 76 | context.onEndResultCalled = true; 77 | defaultContext = context; 78 | }, 79 | }; 80 | 81 | const context1 = {}; 82 | const transformerWithContext1 = { 83 | getInitialContext() { 84 | return context1; 85 | }, 86 | onString(str, context) { 87 | context.onStringCalled = true; 88 | }, 89 | onSubstitution(substitution, resultSoFar, context) { 90 | context.onSubstitutionCalled = true; 91 | }, 92 | onEndResult(endResult, context) { 93 | context.onEndResultCalled = true; 94 | }, 95 | }; 96 | 97 | const context2 = {}; 98 | const transformerWithContext2 = { 99 | getInitialContext() { 100 | return context2; 101 | }, 102 | onString(str, context) { 103 | context.onStringCalled = true; 104 | }, 105 | onSubstitution(substitution, resultSoFar, context) { 106 | context.onSubstitutionCalled = true; 107 | }, 108 | onEndResult(endResult, context) { 109 | context.onEndResultCalled = true; 110 | }, 111 | }; 112 | 113 | const tag = createTag( 114 | transformerWithDefaultContext, 115 | transformerWithContext1, 116 | transformerWithContext2, 117 | ); 118 | 119 | tag`foo${42}`; 120 | 121 | expect(defaultContext).toEqual({ 122 | onStringCalled: true, 123 | onSubstitutionCalled: true, 124 | onEndResultCalled: true, 125 | }); 126 | expect(context1).toEqual({ 127 | onStringCalled: true, 128 | onSubstitutionCalled: true, 129 | onEndResultCalled: true, 130 | }); 131 | expect(context2).toEqual({ 132 | onStringCalled: true, 133 | onSubstitutionCalled: true, 134 | onEndResultCalled: true, 135 | }); 136 | }); 137 | 138 | test('calls the "init" hook each time the tag is called', () => { 139 | const getInitialContext = jest.fn(); 140 | const tag = createTag({ getInitialContext }); 141 | 142 | tag`foo`; 143 | tag`foo`; 144 | 145 | expect(getInitialContext).toHaveBeenCalledTimes(2); 146 | }); 147 | 148 | test("doesn't handle function arguments specially", () => { 149 | const plugin = () => ({ 150 | onEndResult(endResult) { 151 | return endResult.toUpperCase(); 152 | }, 153 | }); 154 | const invalidTag = createTag(plugin); 155 | expect(invalidTag`foo bar`).toBe('foo bar'); 156 | 157 | const properTag = createTag(plugin()); 158 | expect(properTag`foo bar`).toBe('FOO BAR'); 159 | }); 160 | 161 | test('supports pipeline of transformers as both argument list and as array', () => { 162 | const transform1 = { 163 | onSubstitution(substitution) { 164 | return substitution.replace('foo', 'doge'); 165 | }, 166 | }; 167 | const transform2 = { 168 | onEndResult(endResult) { 169 | return endResult.toUpperCase(); 170 | }, 171 | }; 172 | const argumentListTag = createTag(transform1, transform2); 173 | const arrayTag = createTag([transform1, transform2]); 174 | expect(argumentListTag`wow ${'foo'}`).toBe('WOW DOGE'); 175 | expect(arrayTag`bow ${'foo'}`).toBe('BOW DOGE'); 176 | }); 177 | 178 | test('supports tail processing of another tag if first argument to tag is a tag', () => { 179 | const tag = createTag({ 180 | onEndResult(endResult) { 181 | return endResult.toUpperCase().trim(); 182 | }, 183 | }); 184 | const raw = tag(String.raw)` 185 | foo bar 186 | ${500} 187 | `; 188 | expect(raw).toBe('FOO BAR\n 500'); 189 | }); 190 | 191 | test('has the correct order when tail processing', () => { 192 | const upperCaseTag = createTag({ 193 | onEndResult(endResult) { 194 | expect(endResult).toBe('foo bar\n 500'); 195 | return endResult.toUpperCase(); 196 | }, 197 | }); 198 | const trimTag = createTag({ 199 | onEndResult(endResult) { 200 | expect(endResult).toBe('\n foo bar\n 500\n '); 201 | return endResult.trim(); 202 | }, 203 | }); 204 | const raw = upperCaseTag(trimTag)` 205 | foo bar 206 | ${500} 207 | `; 208 | expect(raw).toBe('FOO BAR\n 500'); 209 | }); 210 | 211 | describe('supports using the tag as a plain function', () => { 212 | test('with a string', () => { 213 | let onStringCalls = 0; 214 | let onSubstitutionCalls = 0; 215 | let onEndResultCalls = 0; 216 | const tag = createTag({ 217 | onString(string) { 218 | onStringCalls += 1; 219 | return string.toUpperCase(); 220 | }, 221 | onSubstitution() { 222 | onSubstitutionCalls += 1; 223 | }, 224 | onEndResult(endResult) { 225 | onEndResultCalls += 1; 226 | return endResult.trim(); 227 | }, 228 | }); 229 | const raw = tag(` 230 | foo bar 231 | ${500} 232 | `); 233 | expect(raw).toBe('FOO BAR\n 500'); 234 | expect(onStringCalls).toBe(1); 235 | expect(onSubstitutionCalls).toBe(0); 236 | expect(onEndResultCalls).toBe(1); 237 | }); 238 | 239 | test('with a number', () => { 240 | let onSubstitutionCalls = 0; 241 | const tag = createTag({ 242 | onSubstitution() { 243 | onSubstitutionCalls += 1; 244 | }, 245 | onEndResult(endResult) { 246 | return String(endResult); 247 | }, 248 | }); 249 | const raw = tag(42); 250 | expect(raw).toBe('42'); 251 | expect(onSubstitutionCalls).toBe(0); 252 | }); 253 | }); 254 | 255 | test('transforms substitutions to string as per spec', () => { 256 | const get = jest 257 | .fn() 258 | .mockImplementationOnce((target, prop) => { 259 | expect(prop).toBe(Symbol.toPrimitive); 260 | }) 261 | .mockImplementationOnce((target, prop) => { 262 | expect(prop).toBe('toString'); 263 | }) 264 | .mockImplementationOnce((target, prop) => { 265 | expect(prop).toBe('valueOf'); 266 | return () => 42; 267 | }); 268 | 269 | const val = new Proxy({}, { get }); 270 | const tag = createTag(); 271 | const result = tag`foo ${val} bar`; 272 | 273 | expect(get).toHaveBeenCalledTimes(3); 274 | expect(result).toBe('foo 42 bar'); 275 | }); 276 | 277 | test('accepts other tags as arguments and applies them in order', () => { 278 | const tag1 = createTag({ 279 | onEndResult(string) { 280 | return string + '1'; 281 | }, 282 | }); 283 | const tag2 = createTag({ 284 | onEndResult(string) { 285 | return string + '2'; 286 | }, 287 | }); 288 | const tag3 = createTag({ 289 | onEndResult(string) { 290 | return string + '3'; 291 | }, 292 | }); 293 | const superTag = createTag(tag1, createTag(tag2, tag3), { 294 | onEndResult(string) { 295 | return string + '4'; 296 | }, 297 | }); 298 | 299 | expect(superTag`foo`).toBe('foo1234'); 300 | }); 301 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ![common-tags](media/logo.svg) 2 | 3 | 🔖 A set of **well-tested**, commonly used template literal tag functions for use in ES2015+. 4 | 5 | 🌟 Plus some extra goodies for easily making your own tags. 6 | 7 | ## Example 8 | 9 | ```js 10 | import { html } from 'common-tags'; 11 | 12 | html` 13 |
    14 |

    ${user.name}

    15 |
    16 | ` 17 | ``` 18 | 19 | ## Project Status 20 | 21 | | Info | Badges | 22 | | ---------- | ---------------------------------------- | 23 | | Version | [![github release](https://img.shields.io/github/release/declandewet/common-tags.svg?style=flat-square)](https://github.com/declandewet/common-tags/releases/latest) [![npm version](https://img.shields.io/npm/v/common-tags.svg?style=flat-square)](http://npmjs.org/package/common-tags) | 24 | | License | [![npm license](https://img.shields.io/npm/l/common-tags.svg?style=flat-square)](https://github.com/declandewet/common-tags/blob/master/license.md) | 25 | | Popularity | [![npm downloads](https://img.shields.io/npm/dm/common-tags.svg?style=flat-square)](http://npm-stat.com/charts.html?package=common-tags) | 26 | | Testing | [![Build status](https://ci.appveyor.com/api/projects/status/75eiommx0llt3sgd?svg=true)](https://ci.appveyor.com/project/declandewet/common-tags) [![build status](https://img.shields.io/travis/declandewet/common-tags.svg?style=flat-square)](https://travis-ci.org/declandewet/common-tags) [![codecov.io](https://img.shields.io/codecov/c/gh/declandewet/common-tags.svg?style=flat-square)](https://codecov.io/gh/declandewet/common-tags?branch=master) | 27 | | Quality | [![dependency status](https://img.shields.io/david/declandewet/common-tags.svg?style=flat-square)](https://david-dm.org/declandewet/common-tags) [![dev dependency status](https://img.shields.io/david/dev/declandewet/common-tags.svg?style=flat-square)](https://david-dm.org/declandewet/common-tags#info=devDependencies) | 28 | | Style | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) | 29 | 30 | 31 | 32 | ## Table of Contents 33 | 34 | - [Introduction](#introduction) 35 | - [Why You Should Care](#why-you-should-care) 36 | - [See Who Is Using `common-tags`](#see-who-is-using-common-tags) 37 | - [Installation](#installation) 38 | - [Requirements](#requirements) 39 | - [Instructions](#instructions) 40 | - [With unpkg](#with-unpkg) 41 | - [Usage](#usage) 42 | - [Imports](#imports) 43 | - [Available Tags](#available-tags) 44 | - [`html`](#html) 45 | - [Aliases: `source`, `codeBlock`](#aliases-source-codeblock) 46 | - [`safeHtml`](#safehtml) 47 | - [`oneLine`](#oneline) 48 | - [`oneLineTrim`](#onelinetrim) 49 | - [`stripIndent`](#stripindent) 50 | - [`stripIndents`](#stripindents) 51 | - [`inlineLists`](#inlinelists) 52 | - [`oneLineInlineLists`](#onelineinlinelists) 53 | - [`commaLists`](#commalists) 54 | - [`commaListsOr`](#commalistsor) 55 | - [`commaListsAnd`](#commalistsand) 56 | - [`oneLineCommaLists`](#onelinecommalists) 57 | - [`oneLineCommaListsOr`](#onelinecommalistsor) 58 | - [`oneLineCommaListsAnd`](#onelinecommalistsand) 59 | - [`id`](#id) 60 | - [Advanced Usage](#advanced-usage) 61 | - [Tail Processing](#tail-processing) 62 | - [Using Tags on Regular String Literals](#using-tags-on-regular-string-literals) 63 | - [Type Definitions](#type-definitions) 64 | - [Make Your Own Template Tag](#make-your-own-template-tag) 65 | - [Where It All Starts: createTag](#where-it-all-starts-createtag) 66 | - [The Anatomy of a Transformer](#the-anatomy-of-a-transformer) 67 | - [Plugin Transformers](#plugin-transformers) 68 | - [Plugin Pipeline](#plugin-pipeline) 69 | - [Returning Other Values from a Transformer](#returning-other-values-from-a-transformer) 70 | - [List of Built-in Transformers](#list-of-built-in-transformers) 71 | - [`trimResultTransformer([side])`](#trimresulttransformerside) 72 | - [`stripIndentTransformer([type='initial'])`](#stripindenttransformertypeinitial) 73 | - [`replaceResultTransformer(replaceWhat, replaceWith)`](#replaceresulttransformerreplacewhat-replacewith) 74 | - [`replaceSubstitutionTransformer(replaceWhat, replaceWith)`](#replacesubstitutiontransformerreplacewhat-replacewith) 75 | - [`replaceStringTransformer(replaceWhat, replaceWith)`](#replacestringtransformerreplacewhat-replacewith) 76 | - [`inlineArrayTransformer(opts)`](#inlinearraytransformeropts) 77 | - [`splitStringTransformer(splitBy)`](#splitstringtransformersplitby) 78 | - [How to Contribute](#how-to-contribute) 79 | - [License](#license) 80 | - [Other ES2015 Template Tag Modules](#other-es2015-template-tag-modules) 81 | 82 | 83 | 84 | ## Introduction 85 | 86 | `common-tags` initially started out as two template tags I'd always find myself writing - one for stripping indents, and one for trimming multiline strings down to a single line. In its prime, I was an avid user of [CoffeeScript](http://coffeescript.org), which had this behaviour by default as part of its block strings feature. I also started out programming in Ruby, which has a similar mechanism called Heredocs. 87 | 88 | Over time, I found myself needing a few more template tags to cover edge cases - ones that supported including arrays, or ones that helped to render out tiny bits of HTML not large enough to deserve their own file or an entire template engine. So I packaged all of these up into this module. 89 | 90 | As more features were proposed, and I found myself needing a way to override the default settings to cover even more edge cases, I realized that my initial implementation wouldn't be easy to scale. 91 | 92 | So I re-wrote this module on top of a core architecture that makes use of transformer plugins which can be composed, imported independently and re-used. 93 | 94 | ## Why You Should Care 95 | 96 | Tagged templates in ES2015 are a welcome feature. But, they have their downsides. One such downside is that they preserve all whitespace by default - which makes multiline strings in source code look terrible. 97 | 98 | Source code is not just for computers to interpret. Humans have to read it too 😁. If you care at all about how neat your source code is, or come from a [CoffeeScript](http://coffeescript.org/) background and miss the [block string syntax](http://coffeescript.org/#strings), then you will love `common-tags`, as it was initially intended to bring this feature "back" to JS since its [initial commit](https://github.com/declandewet/common-tags/commit/2595288d6c276439d98d1bcbbb0aa113f4f7cd86). 99 | 100 | `common-tags` also [exposes a means of composing pipelines of dynamic transformer plugins](#plugin-transformers). As someone with a little experience writing tagged templates, I can admit that it is often the case that one tag might need to do the same thing as another tag before doing any further processing; for example - a typical tag that renders out HTML could strip initial indents first, then worry about handling character escapes. Both steps could easily be useful as their own separate template tags, but there isn't an immediately obvious way of composing the two together for maximum re-use. `common-tags` offers not [one](#tail-processing), but [two](#plugin-pipeline) ways of doing this. 101 | 102 | Furthermore, I try to keep this project as transparently stable and updated as frequently as I possibly can. As you may have already seen by the [project status table](#project-status), `common-tags` is linted, well tested, tests are well covered, tests pass on both Unix and Windows operating systems, the popularity bandwidth is easily referenced and dependency health is in plain sight 😄. `common-tags` is also already [used in production on a number of proprietary sites and dependent projects](#see-who-is-using-common-tags), and [contributions are always welcome](#how-to-contribute), as are [suggestions](issues). 103 | 104 | ## See Who Is Using `common-tags` 105 | 106 | - **[Slack](https://slack.com/)** ([ref](https://slack.com/libs/desktop)) 107 | - **[Discord](https://discordapp.com)** ([ref](https://discordapp.com/acknowledgements)) 108 | - **[CircleCI](https://circleci.com)** ([ref](https://circleci.com/docs/2.0/open-source/)) 109 | - **[Confluent](https://www.confluent.io/)** ([ref](https://www.confluent.io/third_party_software/)) 110 | - **[Tessel](https://tessel.io/)** ([ref](https://github.com/tessel/t2-cli/blob/575ddb23f432d10f86b76f5cdca866d1146dedf5/package.json#L56)) 111 | - **[Ember.js](https://www.emberjs.com/)** ([ref](https://github.com/emberjs/ember.js/blob/cacefee49ea4be2621a0ced3e4ceb0010d6cd841/package.json#L93)) 112 | - **[Angular](https://angularjs.org/)** ([ref](https://github.com/angular/angular-cli/blob/90e2e805aae6e0bd2e00e52063221736a8d9cb0c/package.json#L50)) 113 | - **[Prettier](https://prettier.io/)** ([ref](https://github.com/prettier/prettier-eslint/blob/49b762b57b7e7af3b06bd933050c614a91b6742d/package.json#L18)) 114 | - **[Apollo](https://www.apollographql.com)** ([ref](https://github.com/apollographql/apollo-codegen/blob/b9b9a2afd851fa3cba786b26684b26378b1a6f53/package.json#L48)) 115 | - **[Workbox](https://developers.google.com/web/tools/workbox/)** ([ref](https://github.com/GoogleChrome/workbox/blob/d391a0cb51b3e89121c5274fb15f05988233b57e/package.json#L64)) 116 | - **[Gatsby](https://www.gatsbyjs.org/)** ([ref](https://github.com/gatsbyjs/gatsby/blob/3af191c9961b6da1cc04e9cb0a03787af25878db/packages/gatsby-cli/package.json#L16)) 117 | - **[Storybook](https://storybook.js.org/)** ([ref](https://github.com/storybooks/storybook/blob/c275e5c508714bd1a49342e51ddf00bbdb54d277/app/react/package.json#L46)) 118 | - **[Cypress](https://www.cypress.io/)** ([ref](https://github.com/cypress-io/cypress/blob/5d761630f233abb30b9b2e3fede9a4c4887cf880/cli/package.json#L44)) 119 | - **[stylelint](http://stylelint.io/)** ([ref](https://github.com/stylelint/stylelint/blob/5dc5db5599a00cabc875cf99c56d60f93fbbbd2d/package.json#L82)) 120 | - **[pnpm](https://pnpm.js.org/)** ([ref](https://github.com/pnpm/pnpm/blob/36be3d3f0c75992a1f3ff14b60c99115547d0fcc/package.json#L36)) 121 | - **[jss](http://cssinjs.org/)** ([ref](https://github.com/cssinjs/jss/blob/7b9c1222893495c585b4b61d7ca9af05077cefec/package.json#L44)) 122 | - **[BitMidi](https://bitmidi.com/)** ([ref](https://github.com/feross/bitmidi.com/blob/b6cd24f535eaefcbe472063ad5da8418055d77a2/package.json#L68)) 123 | 124 | 125 | ## Installation 126 | 127 | ### Requirements 128 | 129 | The official recommendation for running `common-tags` is as follows: 130 | 131 | - [Node.js](https://nodejs.org/en/download/) v5.0.0 or higher 132 | - In order to use `common-tags`, your environment will also need to support ES2015 tagged templates ([pssst… check Babel out](http://babeljs.io)) 133 | - You might also want to [polyfill some features](https://github.com/zloirock/core-js) if you plan on supporting older browsers: `Array.prototype.includes` 134 | 135 | It might work with below versions of Node, but this is not a guarantee. 136 | 137 | ### Instructions 138 | 139 | `common-tags` is a [Node](https://nodejs.org/) module. So, as long as you have Node.js and NPM installed, installing `common-tags` is as simple as running this in a terminal at the root of your project: 140 | 141 | ```sh 142 | npm install common-tags 143 | ``` 144 | 145 | ### With unpkg 146 | 147 | `common-tags` is also available at [unpkg](https://unpkg.com/common-tags). Just put this code in your HTML: 148 | 149 | ```html 150 | 151 | ``` 152 | 153 | This will make the library available under a global variable `commonTags`. 154 | 155 | ## Usage 156 | 157 | ### Imports 158 | 159 | Like all modules, `common-tags` begins with an `import`. In fact, `common-tags` supports two styles of import: 160 | 161 | **Named imports:** 162 | 163 | ```js 164 | import {stripIndent} from 'common-tags' 165 | ``` 166 | 167 | **Direct module imports:** 168 | 169 | *(Useful if your bundler doesn't support [tree shaking](https://medium.com/@roman01la/dead-code-elimination-and-tree-shaking-in-javascript-build-systems-fb8512c86edf#.p30lbjm94) but you still want to only include modules you need).* 170 | 171 | ```js 172 | import stripIndent from 'common-tags/lib/stripIndent' 173 | ``` 174 | 175 | ### Available Tags 176 | 177 | `common-tags` exports a bunch of wonderful pre-cooked template tags for your eager consumption. They are as follows: 178 | 179 | #### `html` 180 | 181 | ##### Aliases: `source`, `codeBlock` 182 | 183 | You'll often find that you might want to include an array in a template. Typically, doing something like `${array.join(', ')}` would work - but what if you're printing a list of items in an HTML template and want to maintain the indentation? You'd have to count the spaces manually and include them in the `.join()` call - which is a bit *ugly* for my taste. This tag properly indents arrays, as well as newline characters in string substitutions, by converting them to an array split by newline and re-using the same array inclusion logic: 184 | 185 | ```js 186 | import {html} from 'common-tags' 187 | let fruits = ['apple', 'orange', 'watermelon'] 188 | html` 189 |
    190 |
      191 | ${fruits.map(fruit => `
    • ${fruit}
    • `)} 192 | ${'
    • kiwi
    • \n
    • guava
    • '} 193 |
    194 |
    195 | ` 196 | ``` 197 | 198 | Outputs: 199 | 200 | ```html 201 |
    202 |
      203 |
    • apple
    • 204 |
    • orange
    • 205 |
    • watermelon
    • 206 |
    • kiwi
    • 207 |
    • guava
    • 208 |
    209 |
    210 | ``` 211 | 212 | #### `safeHtml` 213 | 214 | A tag very similar to `html` but it does safe HTML escaping for strings coming from substitutions. When combined with regular `html` tag, you can do basic HTML templating that is safe from XSS (Cross-Site Scripting) attacks. 215 | 216 | ```js 217 | import {html, safeHtml} from 'common-tags' 218 | let userMessages = ['hi', 'what are you up to?', ''] 219 | html` 220 |
    221 |
      222 | ${userMessages.map(message => safeHtml`
    • ${message}
    • `)} 223 |
    224 |
    225 | ` 226 | ``` 227 | 228 | Outputs: 229 | 230 | ```html 231 |
    232 |
      233 |
    • hi
    • 234 |
    • what are you up to?
    • 235 |
    • <script>alert("something evil")</script>
    • 236 |
    237 |
    238 | ``` 239 | 240 | #### `oneLine` 241 | 242 | Allows you to keep your single-line strings under 80 characters without resorting to crazy string concatenation. 243 | 244 | ```js 245 | import {oneLine} from 'common-tags' 246 | 247 | oneLine` 248 | foo 249 | bar 250 | baz 251 | ` 252 | // "foo bar baz" 253 | ``` 254 | 255 | #### `oneLineTrim` 256 | 257 | Allows you to keep your single-line strings under 80 characters while trimming the new lines: 258 | 259 | ```js 260 | import {oneLineTrim} from 'common-tags' 261 | 262 | oneLineTrim` 263 | https://news.com/article 264 | ?utm_source=designernews.co 265 | ` 266 | // https://news.com/article?utm_source=designernews.co 267 | ``` 268 | 269 | #### `stripIndent` 270 | 271 | If you want to strip the initial indentation from the beginning of each line in a multiline string: 272 | 273 | ```js 274 | import {stripIndent} from 'common-tags' 275 | 276 | stripIndent` 277 | This is a multi-line string. 278 | You'll ${verb} that it is indented. 279 | We don't want to output this indentation. 280 | But we do want to keep this line indented. 281 | ` 282 | // This is a multi-line string. 283 | // You'll notice that it is indented. 284 | // We don't want to output this indentation. 285 | // But we do want to keep this line indented. 286 | ``` 287 | 288 | Important note: this tag will not indent multiline strings coming from the substitutions. If you want that behavior, use the `html` tag (aliases: `source`, `codeBlock`). 289 | 290 | #### `stripIndents` 291 | 292 | If you want to strip *all* of the indentation from the beginning of each line in a multiline string: 293 | 294 | ```js 295 | import {stripIndents} from 'common-tags' 296 | 297 | stripIndents` 298 | This is a multi-line string. 299 | You'll ${verb} that it is indented. 300 | We don't want to output this indentation. 301 | We don't want to keep this line indented either. 302 | ` 303 | // This is a multi-line string. 304 | // You'll notice that it is indented. 305 | // We don't want to output this indentation. 306 | // We don't want to keep this line indented either. 307 | ``` 308 | 309 | #### `inlineLists` 310 | 311 | Allows you to inline an array substitution as a list: 312 | 313 | ```js 314 | import {inlineLists} from 'common-tags' 315 | 316 | inlineLists` 317 | I like ${['apples', 'bananas', 'watermelons']} 318 | They're good! 319 | ` 320 | // I like apples bananas watermelons 321 | // They're good! 322 | ``` 323 | 324 | #### `oneLineInlineLists` 325 | 326 | Allows you to inline an array substitution as a list, rendered out on a single line: 327 | 328 | ```js 329 | import {oneLineInlineLists} from 'common-tags' 330 | 331 | oneLineInlineLists` 332 | I like ${['apples', 'bananas', 'watermelons']} 333 | They're good! 334 | ` 335 | // I like apples bananas watermelons They're good! 336 | ``` 337 | 338 | #### `commaLists` 339 | 340 | Allows you to inline an array substitution as a comma-separated list: 341 | 342 | ```js 343 | import {commaLists} from 'common-tags' 344 | 345 | commaLists` 346 | I like ${['apples', 'bananas', 'watermelons']} 347 | They're good! 348 | ` 349 | // I like apples, bananas, watermelons 350 | // They're good! 351 | ``` 352 | 353 | #### `commaListsOr` 354 | 355 | Allows you to inline an array substitution as a comma-separated list, the last of which is preceded by the word "or": 356 | 357 | ```js 358 | import {commaListsOr} from 'common-tags' 359 | 360 | commaListsOr` 361 | I like ${['apples', 'bananas', 'watermelons']} 362 | They're good! 363 | ` 364 | // I like apples, bananas or watermelons 365 | // They're good! 366 | ``` 367 | 368 | #### `commaListsAnd` 369 | 370 | Allows you to inline an array substitution as a comma-separated list, the last of which is preceded by the word "and": 371 | 372 | ```js 373 | import {commaListsAnd} from 'common-tags' 374 | 375 | commaListsAnd` 376 | I like ${['apples', 'bananas', 'watermelons']} 377 | They're good! 378 | ` 379 | // I like apples, bananas and watermelons 380 | // They're good! 381 | ``` 382 | 383 | #### `oneLineCommaLists` 384 | 385 | Allows you to inline an array substitution as a comma-separated list, and is rendered out on to a single line: 386 | 387 | ```js 388 | import {oneLineCommaLists} from 'common-tags' 389 | 390 | oneLineCommaLists` 391 | I like ${['apples', 'bananas', 'watermelons']} 392 | They're good! 393 | ` 394 | // I like apples, bananas, watermelons They're good! 395 | ``` 396 | 397 | #### `oneLineCommaListsOr` 398 | 399 | Allows you to inline an array substitution as a comma-separated list, the last of which is preceded by the word "or", and is rendered out on to a single line: 400 | 401 | ```js 402 | import {oneLineCommaListsOr} from 'common-tags' 403 | 404 | oneLineCommaListsOr` 405 | I like ${['apples', 'bananas', 'watermelons']} 406 | They're good! 407 | ` 408 | // I like apples, bananas or watermelons They're good! 409 | ``` 410 | 411 | #### `oneLineCommaListsAnd` 412 | 413 | Allows you to inline an array substitution as a comma-separated list, the last of which is preceded by the word "and", and is rendered out on to a single line: 414 | 415 | ```js 416 | import {oneLineCommaListsAnd} from 'common-tags' 417 | 418 | oneLineCommaListsAnd` 419 | I like ${['apples', 'bananas', 'watermelons']} 420 | They're good! 421 | ` 422 | // I like apples, bananas and watermelons They're good! 423 | ``` 424 | 425 | #### `id` 426 | 427 | A no-op tag that might come in useful in some scenarios, e.g. mocking. 428 | 429 | ```js 430 | import {id} from 'common-tags' 431 | 432 | id`hello ${'world'}` 433 | // hello world 434 | ``` 435 | 436 | ## Advanced Usage 437 | 438 | ### Tail Processing 439 | 440 | It's possible to pass the output of a tagged template to another template tag in pure ES2015+: 441 | 442 | ```js 443 | import {oneLine} from 'common-tags' 444 | 445 | oneLine` 446 | ${String.raw` 447 | foo 448 | bar\nbaz 449 | `} 450 | ` 451 | // "foo bar\nbaz" 452 | ``` 453 | 454 | We can make this neater. Every tag `common-tags` exports can delay execution if it receives a function as its first argument. This function is assumed to be a template tag, and is called via an intermediary tagging process before the result is passed back to our tag. Use it like so (this code is equivalent to the previous code block): 455 | 456 | ```js 457 | import {oneLine} from 'common-tags' 458 | 459 | oneLine(String.raw)` 460 | foo 461 | bar\nbaz 462 | ` 463 | // "foo bar\nbaz" 464 | ``` 465 | 466 | ### Using Tags on Regular String Literals 467 | 468 | Sometimes you might want to use a tag on a normal string (e.g. for stripping the indentation). For that purpose just call a tag as a function with the passed string: 469 | 470 | ```js 471 | import {stripIndent} from 'common-tags' 472 | 473 | stripIndent(" foo\n bar") 474 | // "foo\n bar" 475 | ``` 476 | 477 | ### Type Definitions 478 | 479 | There are third-party type definitions for `common-tags` on [npm](https://www.npmjs.com/package/@types/common-tags). Just install them like so: 480 | 481 | ```sh 482 | npm install @types/common-tags 483 | ``` 484 | 485 | Please note that these type definitions are not officially maintained by the authors of 486 | `common-tags` - they are maintained by the TypeScript community. 487 | 488 | ### Make Your Own Template Tag 489 | 490 | `common-tags` exposes an interface that allows you to painlessly create your own template tags. 491 | 492 | #### Where It All Starts: createTag 493 | 494 | `common-tags` exports a `createTag` function. This function is the foundation of `common-tags`. The concept of the function works on the premise that transformations occur on a template either when the template is finished being processed (`onEndResult`), or when the tag encounters a string (`onString`) or a substitution (`onSubstitution`). Any tag produced by this function supports [tail processing](#tail-processing). 495 | 496 | The easiest tag to create is a tag that does nothing: 497 | 498 | ```js 499 | import {createTag} from 'common-tags' 500 | 501 | const doNothing = createTag() 502 | 503 | doNothing`foo bar` 504 | // 'foo bar' 505 | ``` 506 | 507 | #### The Anatomy of a Transformer 508 | 509 | `createTag` receives either an array or argument list of `transformers`. A `transformer` is just a plain object with three optional methods - `getInitialContext`, `onString`, `onSubstitution` and `onEndResult` - it looks like this: 510 | 511 | ```js 512 | { 513 | getInitialContext () { 514 | // optional. Called before everything else. 515 | // The result of this hook will be passed to other hooks as `context`. 516 | // If omitted, `context` will be an empty object. 517 | }, 518 | onString (str, context) { 519 | // optional. Called when the tag encounters a string. 520 | // (a string is whatever's not inside "${}" in your template literal) 521 | // `str` is the value of the current string 522 | }, 523 | onSubstitution (substitution, resultSoFar, context) { 524 | // optional. Called when the tag encounters a substitution. 525 | // (a substitution is whatever's inside "${}" in your template literal) 526 | // `substitution` is the value of the current substitution 527 | // `resultSoFar` is the end result up to the point of this substitution 528 | }, 529 | onEndResult (endResult, context) { 530 | // optional. Called when all substitutions have been parsed 531 | // `endResult` is the final value. 532 | } 533 | } 534 | ``` 535 | 536 | #### Plugin Transformers 537 | 538 | You can wrap a transformer in a function that receives arguments in order to create a dynamic plugin: 539 | 540 | ```js 541 | const substitutionReplacer = (oldValue, newValue) => ({ 542 | onSubstitution(substitution, resultSoFar) { 543 | if (substitution === oldValue) { 544 | return newValue 545 | } 546 | return substitution 547 | } 548 | }) 549 | 550 | const replaceFizzWithBuzz = createTag(substitutionReplacer('fizz', 'buzz')) 551 | 552 | replaceFizzWithBuzz`foo bar ${"fizz"}` 553 | // "foo bar buzz" 554 | ``` 555 | 556 | #### Plugin Pipeline 557 | 558 | You can pass a list of transformers, and `createTag` will call them on your tag in the order they are specified: 559 | 560 | ```js 561 | // note: passing these as an array also works 562 | const replace = createTag( 563 | substitutionReplacer('fizz', 'buzz'), 564 | substitutionReplacer('foo', 'bar') 565 | ) 566 | 567 | replace`${"foo"} ${"fizz"}` 568 | // "bar buzz" 569 | ``` 570 | 571 | When multiple transformers are passed to `createTag`, they will be iterated three times - first, all transformer `onString` methods will be called. Once they are done processing, `onSubstitution` methods will be called. Finally, all transformer `onEndResult` methods will be called. 572 | 573 | #### Returning Other Values from a Transformer 574 | 575 | All transformers get an additional context argument. You can use it to calculate the value you need: 576 | 577 | ```js 578 | const listSubs = { 579 | getInitialContext() { 580 | return { strings: [], subs: [] } 581 | }, 582 | onString(str, context) { 583 | context.strings.push(str) 584 | return str 585 | }, 586 | onSubstitution(sub, res, context) { 587 | context.subs.push({ sub, precededBy: res }) 588 | return sub 589 | }, 590 | onEndResult(res, context) { 591 | return context 592 | } 593 | } 594 | 595 | const toJSON = { 596 | onEndResult(res) { 597 | return JSON.stringify(res, null, 2) 598 | } 599 | } 600 | 601 | const log = { 602 | onEndResult(res) { 603 | console.log(res) 604 | return res 605 | } 606 | } 607 | 608 | const process = createTag([listSubs, toJSON, log]) 609 | 610 | process` 611 | foo ${'bar'} 612 | fizz ${'buzz'} 613 | ` 614 | // { 615 | // "strings": [ 616 | // "\n foo ", 617 | // "\n foo bar\n fizz ", 618 | // "\n" 619 | // ], 620 | // "subs": [ 621 | // { 622 | // "sub": "bar", 623 | // "precededBy": "\n foo " 624 | // }, 625 | // { 626 | // "sub": "buzz", 627 | // "precededBy": "\n foo bar\n fizz " 628 | // } 629 | // ] 630 | // } 631 | ``` 632 | 633 | #### List of Built-in Transformers 634 | 635 | Since `common-tags` is built on the foundation of this createTag function, it comes with its own set of built-in transformers: 636 | 637 | ##### `trimResultTransformer([side])` 638 | 639 | Trims the whitespace surrounding the end result. Accepts an optional `side` (can be `"start"` or `"end"` or alternatively `"left"` or `"right"`) that when supplied, will only trim whitespace from that side of the string. 640 | 641 | ##### `stripIndentTransformer([type='initial'])` 642 | 643 | Strips the indents from the end result. Offers two types: `all`, which removes all indentation from each line, and `initial`, which removes the shortest indent level from each line. Defaults to `initial`. 644 | 645 | ##### `replaceResultTransformer(replaceWhat, replaceWith)` 646 | 647 | Replaces a value or pattern in the end result with a new value. `replaceWhat` can be a string or a regular expression, `replaceWith` is the new value. 648 | 649 | ##### `replaceSubstitutionTransformer(replaceWhat, replaceWith)` 650 | 651 | Replaces the result of all substitutions (results of calling `${ ... }`) with a new value. Same as for `replaceResultTransformer`, `replaceWhat` can be a string or regular expression and `replaceWith` is the new value. 652 | 653 | ##### `replaceStringTransformer(replaceWhat, replaceWith)` 654 | 655 | Replaces the result of all strings (what's not in `${ ... }`) with a new value. Same as for `replaceResultTransformer`, `replaceWhat` can be a string or regular expression and `replaceWith` is the new value. 656 | 657 | ##### `inlineArrayTransformer(opts)` 658 | 659 | Converts any array substitutions into a string that represents a list. Accepts an options object: 660 | 661 | ```js 662 | opts = { 663 | separator: ',', // what to separate each item with (always followed by a space) 664 | conjunction: 'and', // replace the last separator with this value 665 | serial: true // should the separator be included before the conjunction? As in the case of serial/oxford commas 666 | } 667 | ``` 668 | 669 | ##### `splitStringTransformer(splitBy)` 670 | 671 | Splits a string substitution into an array by the provided `splitBy` substring, **only** if the string contains the `splitBy` substring. 672 | 673 | ## How to Contribute 674 | 675 | Please see the [Contribution Guidelines](contributing.md). 676 | 677 | ## License 678 | 679 | MIT. See [license.md](license.md). 680 | 681 | ## Other ES2015 Template Tag Modules 682 | 683 | If `common-tags` doesn't quite fit your bill, and you just can't seem to find what you're looking for - perhaps these might be of use to you? 684 | 685 | - [tage](https://www.npmjs.com/package/tage) - make functions work as template tags too 686 | - [is-tagged](https://www.npmjs.com/package/is-tagged) - Check whether a function call is initiated by a tagged template string or invoked in a regular way 687 | - [es6-template-strings](https://www.npmjs.com/package/es6-template-strings) - Compile and resolve template strings notation as specified in ES6 688 | - [t7](https://github.com/trueadm/t7) - A light-weight virtual-dom template library 689 | - [html-template-tag](https://www.npmjs.com/package/html-template-tag) - ES6 Tagged Template for compiling HTML template strings. 690 | - [clean-tagged-string](https://www.npmjs.com/package/clean-tagged-string) - A simple utility function to clean ES6 template strings. 691 | - [multiline-tag](https://www.npmjs.com/package/multiline-tag) - Tags for template strings making them behave like coffee multiline strings 692 | - [deindent](https://www.npmjs.com/package/deindent) - ES6 template string helper for deindentation. 693 | - [heredoc-tag](https://www.npmjs.com/package/heredoc-tag) - Heredoc helpers for ES2015 template strings 694 | - [regx](https://www.npmjs.com/package/regx) - Tagged template string regular expression compiler. 695 | - [regexr](https://www.npmjs.org/package/regexr) - Provides an ES6 template tag function that makes it easy to compose regexes out of template strings without double-escaped hell. 696 | - [url-escape-tag](https://www.npmjs.com/package/url-escape-tag) - A template tag for escaping url parameters based on ES2015 tagged templates. 697 | - [shell-escape-tag](https://www.npmjs.com/package/shell-escape-tag) - An ES6+ template tag which escapes parameters for interpolation into shell commands. 698 | - [sql-tags](https://www.npmjs.com/package/sql-tags) - ES6 tagged template string functions for SQL statements. 699 | - [sql-tag](https://www.npmjs.com/package/sql-tag) - A template tag for writing elegant sql strings. 700 | - [sequelize-sql-tag](https://www.npmjs.com/package/sequelize-sql-tag) - A sequelize plugin for sql-tag 701 | - [pg-sql-tag](https://www.npmjs.com/package/pg-sql-tag) - A pg plugin for sql-tag 702 | - [sql-template-strings](https://www.npmjs.com/package/sql-template-strings) - ES6 tagged template strings for prepared statements with mysql and postgres 703 | - [sql-composer](https://www.npmjs.com/package/sql-composer) - Composable SQL template strings for Node.js 704 | - [pg-template-tag](https://www.npmjs.com/package/pg-template-tag) - ECMAScript 6 (2015) template tag function to write queries for node-postgres. 705 | - [digraph-tag](https://www.npmjs.com/package/digraph-tag) - ES6 string template tag for quickly generating directed graph data 706 | - [es2015-i18n-tag](https://www.npmjs.com/package/es2015-i18n-tag) - ES2015 template literal tag for i18n and l10n translation and localization 707 | --------------------------------------------------------------------------------