Skip to content

Commit

Permalink
Init
Browse files Browse the repository at this point in the history
  • Loading branch information
stkao05 committed Apr 7, 2020
0 parents commit 1a43521
Show file tree
Hide file tree
Showing 24 changed files with 11,491 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const config = {
extends: [
"eslint:recommended",
"plugin:react/recommended",
"prettier",
"prettier/react",
"plugin:compat/recommended"
],
env: {
browser: true,
"jest/globals": true
},
parserOptions: {
ecmaFeatures: {
jsx: true,
impliedStrict: true
},
sourceType: "module"
},
plugins: ["react", "prettier", "react-hooks", "jest"],
parser: "babel-eslint",
rules: {
semi: ["error", "never"],
"arrow-parens": ["error", "as-needed"],
"prettier/prettier": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "error"
},
settings: {
react: {
version: "16.0"
}
}
}

module.exports = config
32 changes: 32 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: CI

on:
push:
branches: [ master ]
pull_request:
branches: [ master ]

jobs:
build:

runs-on: ubuntu-latest

strategy:
matrix:
node-version: [12.x]

steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run build
- run: npm test
env:
CI: true
- run: npm run lint
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
9 changes: 9 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"printWidth": 80,
"tabWidth": 4,
"semi": false,
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid",
"proseWrap": "never"
}
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Dependencies and target environment

One core goal this library is to deliver a lightweight solution. Therefore, it comes to critically when it comes to user of dependencies and also polyfill usage.

- `react` should be its only dependencies.
- No polyfill.

136 changes: 136 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# react-interpolate

[![size](http://img.badgesize.io/https://cdn.jsdelivr.net/gh/Doist/react-interpolate/dist/react-interpolate.min.cjs?compression=gzip)](http://img.badgesize.io/https://cdn.jsdelivr.net/gh/Doist/react-interpolate/dist/react-interpolate.min.cjs?compression=gzip)
[![Actions Status](https://github.com/Doist/react-interpolate/workflows/CI/badge.svg)](https://github.com/Doist/react-interpolate/actions)


A string interpolation component that formats and interpolates a template string in a safe way.

```jsx
import Interpolate from "@doist/react-interpolate"

function Greeting() {
return <Interpolate
string="<h1>Hello {name}. Here is <a>your order info</a></h1>"
mapping={
a={child => <a href="https://orderinfo.com">{child}</a>)
name="William"
}
/>
}
```
Would render the following HTML
```html
<h1>Hello William. Here is <a href="https://orderinfo.com">your order info</a></h1>
```
## Component API
`<Interpolate>` component accepts the following props
#### `string`
The template string to be interpolated. Required.
Please see the [Interpolation syntax](./#interpolation-syntax) section below for more detail.
#### `mapping`
An object that defines the values to be injected for placeholder and tags defined in the template string. Optional.
- You can map placeholder and self-closing tag to any [valid element value](https://reactjs.org/docs/react-api.html#isvalidelement)
- For open & close tag, you need to supply a renderer function that defines how the the enclosed children should be rendered.
```jsx
<Interpolate
string="Hello {name}. Here is <a>your order info</a><hr/>"
mapping={
// you can map placholder and self-closing tag to any valid element value
name="William"
hr={<hr className="break"/>}

// mapping value for open & close tag must be a function
a={children => <a href="https://orderinfo.com">{children}</a>)
}
/>
```
#### `graceful`
A boolean flag indicates how string syntax error or mapping error should be handled. When true, the raw string value from the prop `string` would be rendered as a fallback in the error scenario. When false, error would be thrown instead.
Optional. `true` by default.
```jsx
// would render "an invalid string with unclose tag &lt;h1&gt;"
<Interpolate
graceful
string="an invalid string with unclose tag <h1>"
/>
```
## Interpolation syntax
Here is interpolation syntax you can use in your `string`.
#### Placeholder
```jsx
"hello {user_name}"
```
Placeholder name should be alphanumeric (`[A-Za-z0-9_]`). Placeholders could be mapped to any valid element value.
#### Open & close tags
```jsx
"Here is <a>your order info</a>"

// tag name could be any alphanumeric string
"Here is <link>your order info</link>"

// you can nest tag and placeholder
"Here is <a><b>you order info {name}</b></a>"
```
Tag name should be alphanumeric (`[A-Za-z0-9_]`). Open & close tag could only be mapped to a renderer function.
```jsx
<Interpolate
string="Here is <a>your order info</a>"
mapping={
a={children => <a href="https://orderinfo.com">{children}</a>)
}
/>
```
Unclosed tag or incorrect nesting of tag would result in syntax error.
```js
// bad: no close tag
"Here is <a>your order info"

// bad: incorrect tag structure
"Here is <a><b>your order info</a></b>"
```
#### Self closing tag
```js
"Hello.<br/>Here is your order"
```
Tag name should be alphanumeric (`[A-Za-z0-9_]`). Self closing tags could be mapped to any valid element value.
## Auto tag element creation
When tags are used the string but there are no correponding mapped value, it would by default create the corresponding HTML element by default.
```jsx
// would render: <h1>Hellow</h1><br/>World
<Interpolate string="<h1>Hello</h1><br/>world"/>
```
170 changes: 170 additions & 0 deletions __test__/Interpolate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/* eslint-disable react/display-name */
import React from "react"
import { render } from "@testing-library/react"
import Interpolate from "../src/interpolate"

Interpolate.defaultProps = {
graceful: false
}

const surpressConsole = () => {
const w = jest.spyOn(console, "warn").mockImplementation()
const e = jest.spyOn(console, "error").mockImplementation()

return () => {
w.mockRestore()
e.mockRestore()
}
}

describe("Interpolate", () => {
function renderTest({ expected, ...props }) {
return () => {
const { container } = render(<Interpolate {...props} />)
expect(container.innerHTML).toEqual(expected)
}
}

test("when no mapping is provide", () => {
const restore = surpressConsole() // Interpolate will output warning when no mapping is provided

renderTest({
string: "<h1>hello <b>{name}</b></h1><br/>. welcome to todoist",
expected: "<h1>hello <b>{name}</b></h1><br>. welcome to todoist"
})

restore()
})

test(
"tag mapping",
renderTest({
string: "<h1>hello <b>steven</b></h1>. welcome to todoist",
mapping: {
b: child => <i>{child}</i>,
h1: child => <h2>{child}</h2>
},
expected: "<h2>hello <i>steven</i></h2>. welcome to todoist"
})
)

test(
"placholder mapping",
renderTest({
string: "{greeting} <b>{name}</b>. welcome to todoist",
mapping: {
greeting: "hi",
name: () => <i>steven</i>
},
expected: "hi <b><i>steven</i></b>. welcome to todoist"
})
)

test(
"void tag mapping",
renderTest({
string: "hello <br/>",
mapping: {
br: <hr />
},
expected: "hello <hr>"
})
)

test(
"combination of mapping",
renderTest({
string: "<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist",
mapping: {
h1: child => <h2>{child}</h2>,
b: child => <i>{child}</i>,
name: "steven",
br: <hr />
},
expected: "<h2>hello <i>steven</i></h2>.<hr> welcome to todoist"
})
)

test("combination of mapping with function component", () => {
// eslint-disable-next-line
const Subheader = ({ children }) => {
return <h2 className="subheader">{children}</h2>
}

renderTest({
string: "<h1>hello <b>{name}</b></h1>.<br/> welcome to todoist",
mapping: {
h1: child => <Subheader>{child}</Subheader>,
b: child => <i>{child}</i>,
name: "steven",
br: <hr />
},
expected:
'<h2 class="subheader">hello <i>steven</i></h2>.<hr> welcome to todoist'
})
})

test(
"spacing in the void tag and placeholder should be allowed",
renderTest({
string: "hello { name }<br /> welcome to todoist",
mapping: {
name: "steven",
br: <hr />
},
expected: "hello steven<hr> welcome to todoist"
})
)

test("the mapping value should be interpolate corrected with proper html escape", () => {
renderTest({
string: "hello { name }<br /> welcome to todoist",
mapping: {
name: "<script>window.xss = 1</script>",
br: "<script>window.xss = 1</script>"
},
expected:
"hello &lt;script&gt;window.xss = 1&lt;/script&gt;&lt;script&gt;window.xss = 1&lt;/script&gt; welcome to todoist"
})()

expect(window.css).toBeUndefined()
})

test("when graceful flag is on and string contains syntax error, interpolate should return the original string and should not throw error", () => {
const restore = surpressConsole()

renderTest({
string: "</h1>",
expected: "&lt;/h1&gt;",
graceful: true
})()

restore()
})
})

describe("Interpolate: error", () => {
let restore
beforeAll(() => {
restore = surpressConsole()
})
afterAll(() => {
restore()
})

function renderTest({ props }) {
return () => {
expect(() => render(<Interpolate {...props} />)).toThrow()
}
}

test("non-closing tag", renderTest({ string: "<b>" }))

test(
"mapping value for tag should always be a function",
renderTest({
string: "<h1>hello</h1>. welcome to todoist",
mapping: { h1: "hi" }
})
)
})
Loading

0 comments on commit 1a43521

Please sign in to comment.